# EdpConfigProviderMessageHandlerBuilder

Builders that wire an `edpconfigprovider` config (and, optionally, a per-tenant
config) into a `messagehandler.MessageHandler`. The builders resolve the tenant
from the incoming `Message`/`Event` headers, populate the thread-local context,
and hand the resolved config(s) to your handler factory.

## Exports

- `ConfigProviderMessageHandlerBuilder[T]` — single-config builder.
- `MultiTenantConfigProviderMessageHandlerBuilder[T, U]` — extends the base
  builder with a per-tenant config (`U`) resolved via an
  `EnvTenantConfigFactory[U]` and cached per tenant.

## How it works

`build_message_handler(message, event)`:

1. Reads `tenant` (falling back to `context`) from `message.headers` or
   `event.headers`.
2. If absent and `tenant_mandatory=True`, raises `RuntimeError("No tenant")`.
   Otherwise sets `_edp_tenant_name` / `_edp_context_name` on the current
   thread and, when the bus is a `MultiContextMessageBus`, calls
   `set_current_context(tenant_name)`.
3. Lazily builds the global config (`config_transformer.build_config()`,
   memoized) and calls `build_message_handler_with_config(config)`.
4. Any exception raised inside the pipeline is routed through
   `handle_exception` — if `exception_handler` was provided, it is invoked
   with the exception and its return value is returned; otherwise the
   exception is re-raised.

The multi-tenant variant additionally:

- Looks up `_edp_tenant_name` from the current thread inside
  `build_message_handler_with_config`.
- Resolves a per-tenant `U` via `tenant_config_factory.build_config(tenant)`,
  caching the result in `tenant_dict` with double-checked locking (a global
  `_cache_lock` guards a per-tenant `Lock` map).
- Delegates to the abstract
  `build_message_handler_with_tenant_config(config, tenant_config)`.
- If `build_tenant` returns `None`, the result is **not** cached and an error
  is logged; the call falls through to `handle_exception` with
  `Exception("No tenant")`.

## Usage

### Single-config builder

```python
from edpconfigprovidermessagehandler import ConfigProviderMessageHandlerBuilder
from messagehandler import MessageHandler


class MyHandlerBuilder(ConfigProviderMessageHandlerBuilder[MyConfig]):
    def build_message_handler_with_config(self, config: MyConfig) -> MessageHandler:
        return MyHandler(config)


builder = MyHandlerBuilder(
    config_transformer=my_config_mapper,
    message_bus=bus,
    tenant_mandatory=True,
    exception_handler=lambda ex: NoopHandler(),  # optional
)
handler = builder.build_message_handler(message=msg, event=None)
```

### Multi-tenant builder

```python
from edpconfigprovidermessagehandler import MultiTenantConfigProviderMessageHandlerBuilder
from edpconfigprovider.util.environment import EnvTenantConfigFactory
from messagehandler import MessageHandler


class MyMultiTenantBuilder(
    MultiTenantConfigProviderMessageHandlerBuilder[MyConfig, MyTenantConfig]
):
    def build_message_handler_with_tenant_config(
        self,
        config: MyConfig,
        tenant_config: MyTenantConfig,
    ) -> MessageHandler:
        return MyHandler(config, tenant_config)


builder = MyMultiTenantBuilder(
    config_transformer=my_config_mapper,
    tenant_config_factory=EnvTenantConfigFactory(...),
    message_bus=bus,
    tenant_mandatory=True,
    exception_handler=lambda ex: NoopHandler(),  # optional
)
handler = builder.build_message_handler(message=msg, event=None)
```

## Constructor parameters

| Parameter | Type | Notes |
|---|---|---|
| `config_transformer` | `ConfigMapper[T]` | Builds the global config. Result is cached on the builder. |
| `tenant_config_factory` | `EnvTenantConfigFactory[U]` | *(multi-tenant only)* Builds the per-tenant config. |
| `message_bus` | `Optional[MessageBus]` | If a `MultiContextMessageBus`, its current context is switched to the resolved tenant. |
| `tenant_mandatory` | `bool` (default `True`) | When `True`, missing tenant raises `RuntimeError("No tenant")`. |
| `exception_handler` | `Optional[Callable[[Exception], Any]]` | Intercepts all exceptions from the build pipeline; if `None`, exceptions propagate. |

## Extension points

- `build_message_handler_with_config(config)` — required on the base builder.
- `build_message_handler_with_tenant_config(config, tenant_config)` — required
  on the multi-tenant builder.
- `build_tenant(tenant_name)` — defaults to
  `tenant_config_factory.build_config(tenant_name)`; override to customize
  tenant resolution.
