"""
Ch9 配套代码 1 / 3 —— Sentinel 客户端用法

演示：
  1. 用 redis-py 的 Sentinel 类连接哨兵集群
  2. master_for() / slave_for() 自动找当前主 / 从
  3. 故障转移后自动重连，业务无感
  4. 如果本地没有哨兵环境，自动降级为 mock 演示 API 用法

前置：
  pip install redis>=4.0
  典型哨兵端口约定：26379 / 26380 / 26381
"""

import time
from typing import List, Tuple

try:
    import redis
    from redis.sentinel import Sentinel, MasterNotFoundError
    HAS_REDIS = True
except ImportError:
    HAS_REDIS = False


SENTINELS: List[Tuple[str, int]] = [
    ("127.0.0.1", 26379),
    ("127.0.0.1", 26380),
    ("127.0.0.1", 26381),
]
MASTER_NAME = "mymaster"


def section(title: str) -> None:
    print("\n" + "=" * 60)
    print(title)
    print("=" * 60)


def demo_real_sentinel() -> bool:
    """尝试连真实哨兵；连不上返回 False 走 mock。"""
    section("Demo 1: 连接真实 Sentinel 集群")
    try:
        sentinel = Sentinel(SENTINELS, socket_timeout=0.5)
        master_addr = sentinel.discover_master(MASTER_NAME)
        print(f"  当前主节点地址 : {master_addr}")

        slaves = sentinel.discover_slaves(MASTER_NAME)
        print(f"  当前从节点列表 : {slaves}")

        master = sentinel.master_for(MASTER_NAME, socket_timeout=0.5,
                                     decode_responses=True)
        slave = sentinel.slave_for(MASTER_NAME, socket_timeout=0.5,
                                   decode_responses=True)

        master.set("ch9:hello", "from-sentinel")
        print(f"  主写入成功    : ch9:hello = {master.get('ch9:hello')!r}")

        time.sleep(0.2)
        print(f"  从读取成功    : ch9:hello = {slave.get('ch9:hello')!r}")

        master.delete("ch9:hello")
        return True
    except (MasterNotFoundError, redis.ConnectionError, OSError) as e:
        print(f"  ⚠️  连不上哨兵: {e}")
        print("  ⤳ 切到 mock 模式继续演示 API 用法")
        return False


class _MockSentinel:
    """没有真实哨兵时的演示替身，只为打印 API 调用形态。"""

    def __init__(self, sentinels, socket_timeout):
        self.sentinels = sentinels
        self._master = ("127.0.0.1", 6379)
        self._slaves = [("127.0.0.1", 6380), ("127.0.0.1", 6381)]
        print(f"  [mock] 已配置 {len(sentinels)} 个哨兵")

    def discover_master(self, name):
        print(f"  [mock] SENTINEL get-master-addr-by-name {name}")
        return self._master

    def discover_slaves(self, name):
        print(f"  [mock] SENTINEL slaves {name}")
        return list(self._slaves)

    def trigger_failover(self):
        old = self._master
        self._master, self._slaves[0] = self._slaves[0], self._master
        print(f"  [mock] 触发 failover: master {old} → {self._master}")


def demo_mock_sentinel() -> None:
    section("Demo 2: Mock 模式 —— 不连真实哨兵也能学 API")

    sentinel = _MockSentinel(SENTINELS, socket_timeout=0.5)
    print(f"  当前主    : {sentinel.discover_master(MASTER_NAME)}")
    print(f"  当前从    : {sentinel.discover_slaves(MASTER_NAME)}")

    print("\n  ── 模拟一次主节点故障转移 ──")
    sentinel.trigger_failover()

    print(f"  failover 后主    : {sentinel.discover_master(MASTER_NAME)}")
    print(f"  failover 后从    : {sentinel.discover_slaves(MASTER_NAME)}")

    print("\n  💡 真实使用时，redis-py 的 Sentinel 类会订阅 +switch-master")
    print("     频道，主切换后内部连接池自动重建，业务调用 master_for() 拿到")
    print("     的代理对象会无感地连到新主，不需要手工重连。")


def demo_retry_pattern() -> None:
    section("Demo 3: 业务调用层的重试 + 退避模板")

    code = """\
def safe_set(master, key, value, retries=3):
    delay = 0.1
    for i in range(retries):
        try:
            return master.set(key, value)
        except (redis.ConnectionError, redis.TimeoutError) as e:
            print(f"  第 {i+1} 次失败: {e}，{delay}s 后重试")
            time.sleep(delay)
            delay *= 2  # 指数退避
    raise RuntimeError("Redis 不可达，触发降级")
"""
    print(code)
    print("  💡 故障转移期间会有 30~60s 的写不可用窗口，")
    print("     重试 + 指数退避 + 兜底降级是必备组合。")


if __name__ == "__main__":
    if not HAS_REDIS:
        print("❌ 未安装 redis 库，请先 pip install redis")
        raise SystemExit(1)

    if not demo_real_sentinel():
        demo_mock_sentinel()
    demo_retry_pattern()
