"""
Redis sliding window for short-term conversational memory.

Implements a RPUSH + LTRIM pattern:
- RPUSH appends new messages to the right (tail) of the list
- LTRIM trims the list to the last `window` entries
- LRANGE retrieves all current entries

This gives O(1) append + O(1) trim + O(N) read where N <= window size.

Key format: {tenant_id}:memory:short:{agent_id}:{user_id}

Messages are stored as JSON objects with "role" and "content" keys,
matching the OpenAI chat messages format for direct injection into
the LLM messages array.

Design decisions:
- No TTL: message retention is indefinite per user preference. If TTL-based
  expiry is needed in the future, add it via a separate expiry policy.
- No compression: messages are stored as plain JSON. At 20 messages * ~200
  bytes average, storage per user/agent is ~4KB — negligible.
- Parameterized window: callers control the window size, defaulting to 20.
  This allows future policy changes without code modification.
"""

from __future__ import annotations

import json
import logging

from shared.redis_keys import memory_short_key

logger = logging.getLogger(__name__)


async def get_recent_messages(
    redis: object,
    tenant_id: str,
    agent_id: str,
    user_id: str,
    n: int = 20,
) -> list[dict[str, str]]:
    """
    Retrieve the most recent N messages from the sliding window.

    Returns messages in insertion order (oldest first) — this matches the
    expected LLM message array format where conversation flows chronologically.

    Args:
        redis:     Redis async client (redis.asyncio.Redis or compatible).
        tenant_id: Konstruct tenant identifier.
        agent_id:  Agent UUID string.
        user_id:   End-user identifier (channel-native).
        n:         Maximum number of messages to retrieve. Default 20.
                   Pass a larger value than the window size to get all messages.

    Returns:
        List of message dicts with "role" and "content" keys, oldest first.
        Returns empty list if no messages exist for this key.
    """
    key = memory_short_key(tenant_id, agent_id, user_id)

    # LRANGE -n -1 returns the last n items in insertion order
    raw_messages = await redis.lrange(key, -n, -1)  # type: ignore[union-attr]

    messages: list[dict[str, str]] = []
    for raw in raw_messages:
        try:
            msg = json.loads(raw)
            messages.append({"role": str(msg["role"]), "content": str(msg["content"])})
        except (json.JSONDecodeError, KeyError):
            logger.warning("Malformed message in Redis key %s — skipping", key)

    return messages


async def append_message(
    redis: object,
    tenant_id: str,
    agent_id: str,
    user_id: str,
    role: str,
    content: str,
    window: int = 20,
) -> None:
    """
    Append a message to the sliding window and trim to window size.

    Uses a pipeline to make RPUSH + LTRIM atomic — no race condition
    between append and trim even under concurrent writes.

    Args:
        redis:     Redis async client.
        tenant_id: Konstruct tenant identifier.
        agent_id:  Agent UUID string.
        user_id:   End-user identifier (channel-native).
        role:      Message role: "user" or "assistant".
        content:   Message text content.
        window:    Maximum number of messages to retain. Default 20.
                   After this operation the list will contain at most `window`
                   entries (the most recent ones).
    """
    key = memory_short_key(tenant_id, agent_id, user_id)
    serialized = json.dumps({"role": role, "content": content})

    # Pipeline ensures RPUSH + LTRIM are sent atomically
    pipe = redis.pipeline()  # type: ignore[union-attr]
    pipe.rpush(key, serialized)
    # LTRIM to last `window` entries: keep index -(window) through -1
    pipe.ltrim(key, -window, -1)
    await pipe.execute()
