# Issue #158 Phase 7: Distributed write lock for ClickHouse overlap prevention (L1).
"""Redis-backed distributed lock for ClickHouse cache writes.

Prevents concurrent writes to the same (symbol, threshold) from different
writers (sidecar, gap-backfill, kintsugi). Gracefully degrades to no-op
if Redis is unavailable — L2 (schema dedup) catches overlaps.
"""

from __future__ import annotations

import logging
from collections.abc import Generator
from contextlib import contextmanager
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import redis as redis_lib

    from opendeviationbar.config.network import NetworkConfig

logger = logging.getLogger(__name__)


def _net_cfg() -> NetworkConfig:
    from opendeviationbar.config.network import NetworkConfig
    return NetworkConfig()


_redis_client: redis_lib.Redis | None = None
_redis_init_attempted: bool = False


def _get_redis_client() -> redis_lib.Redis | None:
    """Lazy singleton Redis client. Returns None if disabled or unavailable."""
    global _redis_client, _redis_init_attempted  # noqa: PLW0603

    if _redis_init_attempted:
        return _redis_client

    _redis_init_attempted = True

    from opendeviationbar.config import Settings

    cfg = Settings.get().redis
    if not cfg.enabled:
        logger.debug("Redis write lock disabled (OPENDEVIATIONBAR_REDIS_ENABLED=false)")
        return None

    try:
        import redis

        client = redis.Redis(
            host=cfg.host,
            port=cfg.port,
            db=cfg.db,
            socket_connect_timeout=_net_cfg().redis_socket_timeout_s,
            socket_timeout=_net_cfg().redis_socket_timeout_s,
        )
        client.ping()
        _redis_client = client
        logger.debug("Redis write lock connected: %s:%d", cfg.host, cfg.port)
    except (ImportError, OSError, redis.exceptions.RedisError) as e:
        logger.info(
            "Redis unavailable (%s) — write lock disabled, "
            "L2 schema dedup will catch overlaps",
            e,
        )
        _redis_client = None

    return _redis_client


def reset_client() -> None:
    """Reset the cached Redis client (for testing or reconnection)."""
    global _redis_client, _redis_init_attempted  # noqa: PLW0603
    _redis_client = None
    _redis_init_attempted = False


@contextmanager
def cache_write_lock(
    symbol: str,
    threshold_decimal_bps: int,
    *,
    timeout: int | None = None,
) -> Generator[None]:
    """Distributed lock for ClickHouse writes. No-op if Redis unavailable.

    Parameters
    ----------
    symbol : str
        Trading symbol (e.g., "BTCUSDT").
    threshold_decimal_bps : int
        Threshold in decimal basis points.
    timeout : int | None
        Lock TTL in seconds. Defaults to ``Settings.redis.lock_timeout`` (120s).
        Kintsugi repairs pass a longer value to cover the full
        DELETE→fetch→INSERT sequence including subprocess isolation.

    Yields
    ------
    None
        Lock is held for the duration of the context.
    """
    import redis as redis_lib

    client = _get_redis_client()
    if client is None:
        yield
        return

    from opendeviationbar.config import Settings

    cfg = Settings.get().redis
    lock_key = f"opendeviationbar:write:{symbol}:{threshold_decimal_bps}"
    lock_timeout = timeout if timeout is not None else cfg.lock_timeout
    lock = client.lock(
        lock_key,
        timeout=lock_timeout,
        blocking_timeout=cfg.lock_blocking_timeout,
    )

    acquired = False
    try:
        acquired = lock.acquire(blocking=True)
    except (OSError, redis_lib.exceptions.RedisError) as e:
        logger.warning(
            "Redis lock acquisition failed for %s@%d: %s — proceeding without lock",
            symbol, threshold_decimal_bps, e,
        )

    if not acquired:
        logger.debug(
            "Write lock contention for %s@%d (waited %ds) — proceeding without lock (L2 dedup active)",
            symbol, threshold_decimal_bps, cfg.lock_blocking_timeout,
        )
        yield
        return

    try:
        yield
    finally:
        try:
            lock.release()
        except redis_lib.exceptions.LockNotOwnedError:
            logger.warning(
                "Write lock for %s@%d expired before release (TTL=%ds)",
                symbol, threshold_decimal_bps, cfg.lock_timeout,
            )
        except (OSError, redis_lib.exceptions.RedisError) as e:
            logger.warning(
                "Write lock release failed for %s@%d: %s",
                symbol, threshold_decimal_bps, e,
            )
