"""
Ch13 配套代码 1 / 4 —— 慢查询日志分析器

演示：
  1. 配置 SLOWLOG 阈值
  2. 故意触发几个慢命令（DEBUG SLEEP、LRANGE 大 List）
  3. SLOWLOG GET 解析 6 个字段，按耗时排序输出 Top N
  4. 给出每条慢命令的优化建议
"""

import time
import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


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


def setup_slowlog(threshold_us: int = 1000, max_len: int = 1024) -> None:
    section(f"Step 1: 配置 SLOWLOG 阈值 = {threshold_us}us，容量 = {max_len}")
    r.config_set("slowlog-log-slower-than", threshold_us)
    r.config_set("slowlog-max-len", max_len)
    r.execute_command("SLOWLOG", "RESET")
    print(f"  当前 SLOWLOG 长度：{r.execute_command('SLOWLOG', 'LEN')}")


def trigger_slow_commands() -> None:
    section("Step 2: 故意触发慢命令（请稍等 ~6 秒）")

    print("  - 模拟 DEBUG SLEEP 0.05（50ms）")
    try:
        r.execute_command("DEBUG", "SLEEP", "0.05")
    except redis.ResponseError as e:
        print(f"    DEBUG 命令被禁用：{e}")

    print("  - 准备一个 5w 元素的 List，再 LRANGE 0 -1")
    big_list = "demo:slowlog:biglist"
    r.delete(big_list)
    pipe = r.pipeline(transaction=False)
    for i in range(50000):
        pipe.rpush(big_list, f"item-{i}")
    pipe.execute()
    r.lrange(big_list, 0, -1)

    print("  - 准备一个 1w 字段的 Hash，再 HGETALL")
    big_hash = "demo:slowlog:bighash"
    r.delete(big_hash)
    mapping = {f"f{i}": f"v{i}" for i in range(10000)}
    r.hset(big_hash, mapping=mapping)
    r.hgetall(big_hash)

    print("  - 模拟 KEYS *（不要在生产用！）")
    r.keys("*")

    r.delete(big_list, big_hash)


SUGGESTION_RULES = [
    ("KEYS",      "✗ 全库扫描，O(N)。改用 SCAN 0 MATCH pattern COUNT 100"),
    ("HGETALL",   "✗ 大 Hash 全量返回。改用 HSCAN 渐进式遍历，或拆分 Hash"),
    ("SMEMBERS",  "✗ 大 Set 全量返回。改用 SSCAN 渐进式遍历"),
    ("LRANGE",    "✗ 大 List 范围读。LRANGE 0 -1 改成分页 LRANGE 0 99"),
    ("SORT",      "✗ 服务端排序复杂度高。建议业务侧排序"),
    ("FLUSHALL",  "✗ 清空全库。生产应禁用，或改为 FLUSHDB ASYNC"),
    ("FLUSHDB",   "✗ 清空当前 db。改为 FLUSHDB ASYNC（4.0+）"),
    ("DEL",       "△ 大 Key DEL 阻塞主线程。改用 UNLINK"),
    ("DEBUG",     "△ DEBUG 命令仅开发环境，生产应 rename-command 禁用"),
    ("SUNIONSTORE", "△ 大集合求并集 O(N)。考虑拆批"),
    ("SINTERSTORE", "△ 大集合求交集 O(N)。考虑拆批"),
    ("EVAL",      "△ Lua 脚本未限时，可能长时间阻塞主线程"),
]


def suggest(cmd: str) -> str:
    upper = cmd.upper()
    for keyword, msg in SUGGESTION_RULES:
        if keyword in upper:
            return msg
    return "（无特定建议，关注耗时是否合理）"


def show_slowlog(top_n: int = 10) -> None:
    section(f"Step 3: SLOWLOG GET —— 取耗时 Top {top_n}")
    raw = r.execute_command("SLOWLOG", "GET", top_n)
    if not raw:
        print("  （SLOWLOG 为空，可能是阈值太高或没有触发）")
        return

    rows = []
    for entry in raw:
        sid       = entry[0]
        ts        = entry[1]
        duration  = entry[2]
        cmd_parts = [str(p) for p in entry[3]]
        client_ip = entry[4] if len(entry) > 4 else "-"
        client_nm = entry[5] if len(entry) > 5 else "-"
        cmd_str   = " ".join(cmd_parts)
        if len(cmd_str) > 60:
            cmd_str = cmd_str[:57] + "..."
        rows.append((sid, ts, duration, cmd_str, client_ip, client_nm))

    rows.sort(key=lambda x: -x[2])

    print(f"\n  {'ID':>4}  {'时间':<19}  {'耗时(us)':>10}  {'命令':<60}  {'客户端':<22}  名字")
    print("  " + "-" * 130)
    for sid, ts, dur, cmd, ip, nm in rows:
        ts_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
        print(f"  {sid:>4}  {ts_str:<19}  {dur:>10}  {cmd:<60}  {ip:<22}  {nm}")

    print("\n  ─── 优化建议 ───")
    seen = set()
    for sid, ts, dur, cmd, ip, nm in rows:
        cmd_head = cmd.split(" ", 1)[0]
        if cmd_head in seen:
            continue
        seen.add(cmd_head)
        print(f"  [{cmd_head:>10}] {suggest(cmd)}")


def main() -> None:
    try:
        setup_slowlog(threshold_us=1000)
        trigger_slow_commands()
        show_slowlog(top_n=20)

        section("Step 4: 总结")
        print("  ① SLOWLOG 阈值生产建议设为 1000us（1ms）")
        print("  ② 关注 6 个字段：id / 时间 / 耗时 / 命令 / 客户端 IP / 客户端 name")
        print("  ③ KEYS / HGETALL / LRANGE 0 -1 是最常见的「定时炸弹」")
        print("  ④ 用 SCAN 系列 + UNLINK + Pipeline 改造慢命令")
    except redis.ConnectionError as e:
        print(f"❌ Redis 连接失败：{e}")


if __name__ == "__main__":
    main()
