"""Response-callback dictionaries and the protocol/legacy selector.

This module is the single source of truth for the mapping between Redis
command names and the Python-side callbacks that post-process raw parser
output into user-facing values.

Six dictionaries are defined:

* ``_RedisCallbacks`` — entries that produce the same Python value
  regardless of wire protocol or legacy-response selection.
* ``_RedisCallbacksRESP2`` — RESP2 wire, legacy Python shapes.
* ``_RedisCallbacksRESP3`` — RESP3 wire, Python shapes from the previous
  RESP3 callbacks.
* ``_RedisCallbacksRESP2Unified`` — RESP2 wire, unified Python shapes
  (``legacy_responses=False``).
* ``_RedisCallbacksRESP3Unified`` — RESP3 wire, unified Python shapes
  (``legacy_responses=False``).
* ``_RedisCallbacksRESP3toRESP2Legacy`` — RESP3 wire converted back to the
  legacy RESP2 Python shapes for users who keep ``legacy_responses=True``
  on a RESP3 connection.

``get_response_callbacks`` merges ``_RedisCallbacks`` with the appropriate
protocol-specific overlay. Callers wrap the returned dict in
``CaseInsensitiveDict``.
"""

from typing import Any, Callable, Optional

from redis.utils import str_if_bytes

from .helpers import (
    bool_ok,
    bzpop_score_resp3_to_resp2_legacy,
    bzpop_score_unified,
    float_or_none,
    hrandfield_resp3_to_resp2_legacy,
    hrandfield_unified,
    pairs_to_dict,
    parse_acl_getuser,
    parse_acl_getuser_resp3_to_resp2_legacy,
    parse_acl_getuser_unified,
    parse_acl_log,
    parse_acl_log_resp3_to_resp2_legacy,
    parse_acl_log_resp3_unified,
    parse_arinfo,
    parse_client_info,
    parse_client_kill,
    parse_client_list,
    parse_client_trackinginfo_resp3_to_resp2_legacy,
    parse_client_trackinginfo_unified,
    parse_cluster_info,
    parse_cluster_links_resp3_to_resp2_legacy,
    parse_cluster_links_unified,
    parse_cluster_nodes,
    parse_command,
    parse_command_resp3,
    parse_command_unified,
    parse_config_get,
    parse_config_get_resp3_to_resp2_legacy,
    parse_debug_object,
    parse_function_list_resp3_to_resp2_legacy,
    parse_function_list_unified,
    parse_geopos_resp3_to_resp2_legacy,
    parse_geopos_unified,
    parse_geosearch_generic,
    parse_geosearch_generic_unified,
    parse_hscan,
    parse_info,
    parse_lcs_idx_resp3_to_resp2_legacy,
    parse_lcs_idx_unified,
    parse_list_of_dicts,
    parse_list_of_dicts_resp3,
    parse_memory_stats,
    parse_memory_stats_resp3,
    parse_memory_stats_unified,
    parse_pubsub_numsub,
    parse_scan,
    parse_sentinel_get_master,
    parse_sentinel_master,
    parse_sentinel_master_resp3_to_resp2_legacy,
    parse_sentinel_master_unified,
    parse_sentinel_master_unified_resp3,
    parse_sentinel_masters,
    parse_sentinel_masters_resp3,
    parse_sentinel_masters_resp3_to_resp2_legacy,
    parse_sentinel_masters_unified,
    parse_sentinel_masters_unified_resp3,
    parse_sentinel_slaves_and_sentinels,
    parse_sentinel_slaves_and_sentinels_resp3,
    parse_sentinel_slaves_and_sentinels_resp3_to_resp2_legacy,
    parse_sentinel_slaves_and_sentinels_unified,
    parse_sentinel_slaves_and_sentinels_unified_resp3,
    parse_sentinel_state_resp3,
    parse_set_result,
    parse_slowlog_get,
    parse_stralgo,
    parse_stralgo_resp3_unified,
    parse_stralgo_unified,
    parse_stream_list,
    parse_xautoclaim,
    parse_xclaim,
    parse_xinfo_stream,
    parse_xpending,
    parse_xread,
    parse_xread_resp3,
    parse_xread_resp3_to_resp2_legacy,
    parse_xread_unified,
    parse_zadd,
    parse_zmscore,
    parse_zscan,
    parse_zscan_unified,
    sort_return_tuples,
    string_keys_to_dict,
    timestamp_to_datetime,
    zmpop_resp3_to_resp2_legacy,
    zmpop_unified,
    zpop_score_pairs,
    zpop_score_pairs_resp3_to_resp2_legacy,
    zpop_score_pairs_resp3_unified,
    zpop_score_pairs_unified,
    zset_score_for_rank,
    zset_score_for_rank_resp3,
    zset_score_for_rank_resp3_to_resp2_legacy,
    zset_score_for_rank_unified,
    zset_score_pairs,
    zset_score_pairs_resp3,
    zset_score_pairs_resp3_to_resp2_legacy,
    zset_score_pairs_resp3_to_resp2_legacy_flat,
    zset_score_pairs_unified,
)

_RedisCallbacks = {
    **string_keys_to_dict(
        "AUTH COPY EXPIRE EXPIREAT HEXISTS HMSET MOVE MSETNX PERSIST PSETEX "
        "PEXPIRE PEXPIREAT RENAMENX SETEX SETNX SMOVE",
        bool,
    ),
    **string_keys_to_dict("HINCRBYFLOAT INCRBYFLOAT", float),
    **string_keys_to_dict(
        "ASKING FLUSHALL FLUSHDB LSET LTRIM MSET PFMERGE READONLY READWRITE "
        "RENAME SAVE SELECT SHUTDOWN SLAVEOF SWAPDB WATCH UNWATCH",
        bool_ok,
    ),
    **string_keys_to_dict("XREAD XREADGROUP", parse_xread),
    **string_keys_to_dict(
        "GEORADIUS GEORADIUSBYMEMBER GEOSEARCH",
        parse_geosearch_generic,
    ),
    **string_keys_to_dict("XRANGE XREVRANGE", parse_stream_list),
    "ACL GETUSER": parse_acl_getuser,
    "ACL LOAD": bool_ok,
    "ACL LOG": parse_acl_log,
    "ACL SETUSER": bool_ok,
    "ACL SAVE": bool_ok,
    "CLIENT INFO": parse_client_info,
    "CLIENT KILL": parse_client_kill,
    "CLIENT LIST": parse_client_list,
    "CLIENT PAUSE": bool_ok,
    "CLIENT SETINFO": bool_ok,
    "CLIENT SETNAME": bool_ok,
    "CLIENT UNBLOCK": bool,
    "CLUSTER ADDSLOTS": bool_ok,
    "CLUSTER ADDSLOTSRANGE": bool_ok,
    "CLUSTER DELSLOTS": bool_ok,
    "CLUSTER DELSLOTSRANGE": bool_ok,
    "CLUSTER FAILOVER": bool_ok,
    "CLUSTER FORGET": bool_ok,
    "CLUSTER INFO": parse_cluster_info,
    "CLUSTER MEET": bool_ok,
    "CLUSTER NODES": parse_cluster_nodes,
    "CLUSTER REPLICAS": parse_cluster_nodes,
    "CLUSTER REPLICATE": bool_ok,
    "CLUSTER RESET": bool_ok,
    "CLUSTER SAVECONFIG": bool_ok,
    "CLUSTER SET-CONFIG-EPOCH": bool_ok,
    "CLUSTER SETSLOT": bool_ok,
    "CLUSTER SLAVES": parse_cluster_nodes,
    "COMMAND": parse_command,
    "CONFIG RESETSTAT": bool_ok,
    "CONFIG SET": bool_ok,
    "FUNCTION DELETE": bool_ok,
    "FUNCTION FLUSH": bool_ok,
    "FUNCTION RESTORE": bool_ok,
    "GEODIST": float_or_none,
    "HSCAN": parse_hscan,
    "INFO": parse_info,
    "LASTSAVE": timestamp_to_datetime,
    "MEMORY PURGE": bool_ok,
    "MODULE LOAD": bool,
    "MODULE UNLOAD": bool,
    "PING": lambda r: str_if_bytes(r) == "PONG",
    "PUBSUB NUMSUB": parse_pubsub_numsub,
    "PUBSUB SHARDNUMSUB": parse_pubsub_numsub,
    "QUIT": bool_ok,
    "SET": parse_set_result,
    "SCAN": parse_scan,
    "SCRIPT EXISTS": lambda r: list(map(bool, r)),
    "SCRIPT FLUSH": bool_ok,
    "SCRIPT KILL": bool_ok,
    "SCRIPT LOAD": str_if_bytes,
    "SENTINEL CKQUORUM": bool_ok,
    "SENTINEL FAILOVER": bool_ok,
    "SENTINEL FLUSHCONFIG": bool_ok,
    "SENTINEL GET-MASTER-ADDR-BY-NAME": parse_sentinel_get_master,
    "SENTINEL MONITOR": bool_ok,
    "SENTINEL RESET": bool_ok,
    "SENTINEL REMOVE": bool_ok,
    "SENTINEL SET": bool_ok,
    "SLOWLOG GET": parse_slowlog_get,
    "SLOWLOG RESET": bool_ok,
    "SORT": sort_return_tuples,
    "SSCAN": parse_scan,
    "TIME": lambda x: (int(x[0]), int(x[1])),
    "XAUTOCLAIM": parse_xautoclaim,
    "XCLAIM": parse_xclaim,
    "XGROUP CREATE": bool_ok,
    "XGROUP DESTROY": bool,
    "XGROUP SETID": bool_ok,
    "ARINFO": parse_arinfo,
    "XINFO STREAM": parse_xinfo_stream,
    "XPENDING": parse_xpending,
    "ZSCAN": parse_zscan,
}


_RedisCallbacksRESP2 = {
    **string_keys_to_dict(
        "SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
    ),
    **string_keys_to_dict(
        "ZINTER ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION",
        zset_score_pairs,
    ),
    **string_keys_to_dict("ZPOPMAX ZPOPMIN", zpop_score_pairs),
    **string_keys_to_dict(
        "ZREVRANK ZRANK",
        zset_score_for_rank,
    ),
    **string_keys_to_dict("ZINCRBY ZSCORE", float_or_none),
    **string_keys_to_dict("BGREWRITEAOF BGSAVE", lambda r: True),
    **string_keys_to_dict("BLPOP BRPOP", lambda r: r and tuple(r) or None),
    **string_keys_to_dict(
        "BZPOPMAX BZPOPMIN", lambda r: r and (r[0], r[1], float(r[2])) or None
    ),
    "ACL CAT": lambda r: list(map(str_if_bytes, r)),
    "ACL GENPASS": str_if_bytes,
    "ACL HELP": lambda r: list(map(str_if_bytes, r)),
    "ACL LIST": lambda r: list(map(str_if_bytes, r)),
    "ACL USERS": lambda r: list(map(str_if_bytes, r)),
    "ACL WHOAMI": str_if_bytes,
    "CLIENT GETNAME": str_if_bytes,
    "CLIENT TRACKINGINFO": lambda r: list(map(str_if_bytes, r)),
    "CLUSTER GETKEYSINSLOT": lambda r: list(map(str_if_bytes, r)),
    "COMMAND GETKEYS": lambda r: list(map(str_if_bytes, r)),
    "CONFIG GET": parse_config_get,
    "DEBUG OBJECT": parse_debug_object,
    "GEOHASH": lambda r: list(map(str_if_bytes, r)),
    "GEOPOS": lambda r: list(
        map(lambda ll: (float(ll[0]), float(ll[1])) if ll is not None else None, r)
    ),
    "HGETALL": lambda r: r and pairs_to_dict(r) or {},
    "HOTKEYS GET": lambda r: [pairs_to_dict(m) for m in r],
    "MEMORY STATS": parse_memory_stats,
    "MODULE LIST": lambda r: [pairs_to_dict(m) for m in r],
    "RESET": str_if_bytes,
    "SENTINEL MASTER": parse_sentinel_master,
    "SENTINEL MASTERS": parse_sentinel_masters,
    "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels,
    "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels,
    "STRALGO": parse_stralgo,
    "XINFO CONSUMERS": parse_list_of_dicts,
    "XINFO GROUPS": parse_list_of_dicts,
    "ZADD": parse_zadd,
    "ZMSCORE": parse_zmscore,
}


_RedisCallbacksRESP3 = {
    **string_keys_to_dict(
        "SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
    ),
    **string_keys_to_dict(
        "ZRANGE ZINTER ZPOPMAX ZPOPMIN HGETALL XREADGROUP",
        lambda r, **kwargs: r,
    ),
    **string_keys_to_dict(
        "ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION",
        zset_score_pairs_resp3,
    ),
    **string_keys_to_dict(
        "ZREVRANK ZRANK",
        zset_score_for_rank_resp3,
    ),
    **string_keys_to_dict("XREAD XREADGROUP", parse_xread_resp3),
    "ACL LOG": lambda r: (
        [
            {str_if_bytes(key): str_if_bytes(value) for key, value in x.items()}
            for x in r
        ]
        if isinstance(r, list)
        else bool_ok(r)
    ),
    "COMMAND": parse_command_resp3,
    "CONFIG GET": parse_config_get_resp3_to_resp2_legacy,
    "MEMORY STATS": parse_memory_stats_resp3,
    "SENTINEL MASTER": parse_sentinel_state_resp3,
    "SENTINEL MASTERS": parse_sentinel_masters_resp3,
    "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels_resp3,
    "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels_resp3,
    "STRALGO": lambda r, **options: (
        {str_if_bytes(key): str_if_bytes(value) for key, value in r.items()}
        if isinstance(r, dict)
        else str_if_bytes(r)
    ),
    "XINFO CONSUMERS": parse_list_of_dicts_resp3,
    "XINFO GROUPS": parse_list_of_dicts_resp3,
}


# RESP2 wire, unified response shapes (``legacy_responses=False``).
_RedisCallbacksRESP2Unified: dict[str, Callable[..., Any]] = {
    **_RedisCallbacksRESP2,
    **string_keys_to_dict(
        "ZDIFF ZINTER ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION",
        zset_score_pairs_unified,
    ),
    **string_keys_to_dict(
        "ZPOPMAX ZPOPMIN",
        zpop_score_pairs_unified,
    ),
    **string_keys_to_dict(
        "ZREVRANK ZRANK",
        zset_score_for_rank_unified,
    ),
    **string_keys_to_dict(
        "BZPOPMAX BZPOPMIN",
        bzpop_score_unified,
    ),
    **string_keys_to_dict("XREAD XREADGROUP", parse_xread_unified),
    **string_keys_to_dict(
        "GEORADIUS GEORADIUSBYMEMBER GEOSEARCH",
        parse_geosearch_generic_unified,
    ),
    **string_keys_to_dict("BLPOP BRPOP", lambda r: r or None),
    **string_keys_to_dict("ZMPOP BZMPOP", zmpop_unified),
    "ZSCAN": parse_zscan_unified,
    "ZRANDMEMBER": zset_score_pairs_unified,
    "HRANDFIELD": hrandfield_unified,
    "ACL GETUSER": parse_acl_getuser_unified,
    "CLIENT TRACKINGINFO": parse_client_trackinginfo_unified,
    "CLUSTER GETKEYSINSLOT": lambda r, **kwargs: r,
    "CLUSTER LINKS": parse_cluster_links_unified,
    "COMMAND": parse_command_unified,
    "COMMAND GETKEYS": lambda r, **kwargs: r,
    "FUNCTION LIST": parse_function_list_unified,
    "GEOPOS": parse_geopos_unified,
    "LCS": parse_lcs_idx_unified,
    "MEMORY STATS": parse_memory_stats_unified,
    "SENTINEL MASTER": parse_sentinel_master_unified,
    "SENTINEL MASTERS": parse_sentinel_masters_unified,
    "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels_unified,
    "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels_unified,
    "STRALGO": parse_stralgo_unified,
}


# RESP3 wire, unified response shapes (``legacy_responses=False``).
_RedisCallbacksRESP3Unified: dict[str, Callable[..., Any]] = {
    **_RedisCallbacksRESP3,
    **string_keys_to_dict(
        "ZDIFF ZINTER ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION",
        zset_score_pairs_resp3,
    ),
    **string_keys_to_dict(
        "ZPOPMAX ZPOPMIN",
        zpop_score_pairs_resp3_unified,
    ),
    **string_keys_to_dict(
        "BZPOPMAX BZPOPMIN",
        bzpop_score_unified,
    ),
    **string_keys_to_dict("XREAD XREADGROUP", parse_xread_unified),
    **string_keys_to_dict(
        "GEORADIUS GEORADIUSBYMEMBER GEOSEARCH",
        parse_geosearch_generic_unified,
    ),
    "ZSCAN": parse_zscan_unified,
    "ZRANDMEMBER": zset_score_pairs_resp3,
    "ACL CAT": lambda r: list(map(str_if_bytes, r)),
    "ACL GENPASS": str_if_bytes,
    "ACL HELP": lambda r: list(map(str_if_bytes, r)),
    "ACL LIST": lambda r: list(map(str_if_bytes, r)),
    "ACL LOG": parse_acl_log_resp3_unified,
    "ACL USERS": lambda r: list(map(str_if_bytes, r)),
    "ACL WHOAMI": str_if_bytes,
    "CLIENT GETNAME": str_if_bytes,
    "CLIENT TRACKINGINFO": parse_client_trackinginfo_unified,
    "CLUSTER LINKS": parse_cluster_links_unified,
    "COMMAND": parse_command_unified,
    "FUNCTION LIST": parse_function_list_unified,
    "GEOHASH": lambda r: list(map(str_if_bytes, r)),
    "LCS": parse_lcs_idx_unified,
    "RESET": str_if_bytes,
    "SENTINEL MASTER": parse_sentinel_master_unified_resp3,
    "SENTINEL MASTERS": parse_sentinel_masters_unified_resp3,
    "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels_unified_resp3,
    "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels_unified_resp3,
    "STRALGO": parse_stralgo_resp3_unified,
}


# RESP3 wire converted back to the legacy RESP2 Python shapes. Only the
# entries needed to undo RESP3-side differences are listed; everything
# else falls through to ``_RedisCallbacks``. Scores are re-encoded to bytes
# before being passed to ``score_cast_func`` so the callable observes the
# same input type it would on a RESP2 connection.
_RedisCallbacksRESP3toRESP2Legacy: dict[str, Callable[..., Any]] = {
    **string_keys_to_dict(
        "SDIFF SINTER SMEMBERS SUNION", lambda r: r and set(r) or set()
    ),
    **string_keys_to_dict(
        "ZINTER ZRANGE ZRANGEBYSCORE ZREVRANGE ZREVRANGEBYSCORE ZUNION",
        zset_score_pairs_resp3_to_resp2_legacy,
    ),
    "ZDIFF": zset_score_pairs_resp3_to_resp2_legacy_flat,
    "ZRANDMEMBER": zset_score_pairs_resp3_to_resp2_legacy_flat,
    **string_keys_to_dict(
        "ZPOPMAX ZPOPMIN",
        zpop_score_pairs_resp3_to_resp2_legacy,
    ),
    **string_keys_to_dict(
        "BZPOPMAX BZPOPMIN",
        bzpop_score_resp3_to_resp2_legacy,
    ),
    **string_keys_to_dict(
        "ZREVRANK ZRANK",
        zset_score_for_rank_resp3_to_resp2_legacy,
    ),
    **string_keys_to_dict("BGREWRITEAOF BGSAVE", lambda r: True),
    **string_keys_to_dict("XREAD XREADGROUP", parse_xread_resp3_to_resp2_legacy),
    **string_keys_to_dict("BLPOP BRPOP", lambda r: r and tuple(r) or None),
    **string_keys_to_dict("ZMPOP BZMPOP", zmpop_resp3_to_resp2_legacy),
    "HRANDFIELD": hrandfield_resp3_to_resp2_legacy,
    "ACL CAT": lambda r: list(map(str_if_bytes, r)),
    "ACL GENPASS": str_if_bytes,
    "ACL GETUSER": parse_acl_getuser_resp3_to_resp2_legacy,
    "ACL HELP": lambda r: list(map(str_if_bytes, r)),
    "ACL LIST": lambda r: list(map(str_if_bytes, r)),
    "ACL LOG": parse_acl_log_resp3_to_resp2_legacy,
    "ACL USERS": lambda r: list(map(str_if_bytes, r)),
    "ACL WHOAMI": str_if_bytes,
    "CLIENT GETNAME": str_if_bytes,
    "CLIENT TRACKINGINFO": parse_client_trackinginfo_resp3_to_resp2_legacy,
    "CLUSTER GETKEYSINSLOT": lambda r: list(map(str_if_bytes, r)),
    "CLUSTER LINKS": parse_cluster_links_resp3_to_resp2_legacy,
    "COMMAND GETKEYS": lambda r: list(map(str_if_bytes, r)),
    "CONFIG GET": parse_config_get_resp3_to_resp2_legacy,
    "DEBUG OBJECT": parse_debug_object,
    "FUNCTION LIST": parse_function_list_resp3_to_resp2_legacy,
    "GEOHASH": lambda r: list(map(str_if_bytes, r)),
    "GEOPOS": parse_geopos_resp3_to_resp2_legacy,
    "LCS": parse_lcs_idx_resp3_to_resp2_legacy,
    "MEMORY STATS": parse_memory_stats_resp3,
    "RESET": str_if_bytes,
    "SENTINEL MASTER": parse_sentinel_master_resp3_to_resp2_legacy,
    "SENTINEL MASTERS": parse_sentinel_masters_resp3_to_resp2_legacy,
    "SENTINEL SENTINELS": parse_sentinel_slaves_and_sentinels_resp3_to_resp2_legacy,
    "SENTINEL SLAVES": parse_sentinel_slaves_and_sentinels_resp3_to_resp2_legacy,
    "XINFO CONSUMERS": parse_list_of_dicts_resp3,
    "XINFO GROUPS": parse_list_of_dicts_resp3,
}


def get_response_callbacks(
    user_protocol: Optional[int],
    legacy_responses: bool,
) -> dict[str, Callable[..., Any]]:
    """Return the merged callback dict for the given (protocol, legacy)
    combination.

    ``user_protocol`` is the value the user supplied to the client
    constructor (``None`` means "not specified"). ``legacy_responses``
    defaults to ``True`` and selects today's RESP2-style Python shapes
    even when the wire protocol is RESP3.

    Callers wrap the returned dict in ``CaseInsensitiveDict``.
    """
    callbacks: dict[str, Callable[..., Any]] = dict(_RedisCallbacks)
    if legacy_responses:
        if user_protocol is None:
            callbacks.update(_RedisCallbacksRESP3toRESP2Legacy)
        elif user_protocol in (3, "3"):
            callbacks.update(_RedisCallbacksRESP3)
        else:
            callbacks.update(_RedisCallbacksRESP2)
    else:
        if user_protocol is None or user_protocol in (3, "3"):
            callbacks.update(_RedisCallbacksRESP3Unified)
        else:
            callbacks.update(_RedisCallbacksRESP2Unified)
    return callbacks
