"""
Phase 37: Private Agent-to-Agent Direct Messaging.

Enables private 1:1 communication between agents for task coordination,
sensitive data exchange, negotiation, and feedback. Unlike public channels
(state/broker/channels/*.jsonl), DMs are visible only to the two participants.

Features:
  - SEND DM: Private message from one agent to another
  - READ DMS: Retrieve messages with unread tracking via cursors
  - CONVERSATIONS: List all DM threads with unread counts + preview
  - HISTORY: Paginated message history for a conversation
  - MARK READ: Advance read cursor for a conversation
  - BLOCK/UNBLOCK: Prevent unwanted agents from sending DMs
  - BROADCAST DM: Send the same message to multiple agents (individual threads)

Storage:
  state/broker/dms/
    {conversation_key}.jsonl               — append-only message log
    cursors/{session_id}_{conv_key}.cursor  — read position per conversation
    blocks/{session_id}.json                — blocked agent list
"""

import hashlib
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    SESSION_ID,
    SESSION_NAME,
    PLATFORM,
    _append_jsonl_sync,
    _read_jsonl_sync,
    _write_json_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

DMS_DIR = BROKER_DIR / "dms"
DM_CURSORS_DIR = DMS_DIR / "cursors"
DM_BLOCKS_DIR = DMS_DIR / "blocks"

for _d in (DMS_DIR, DM_CURSORS_DIR, DM_BLOCKS_DIR):
    _d.mkdir(parents=True, exist_ok=True)

VALID_MSG_TYPES = ("info", "request", "response", "alert", "ack", "data")


# ── Helpers ────────────────────────────────────────────────────────────────


def _conversation_key(id_a: str, id_b: str) -> str:
    """Deterministic conversation key from two session IDs (sorted pair hash)."""
    pair = "_".join(sorted([id_a, id_b]))
    return hashlib.sha256(pair.encode()).hexdigest()[:16]


def _conversation_file(conv_key: str) -> Path:
    """Path to the JSONL message log for a conversation."""
    return DMS_DIR / f"{conv_key}.jsonl"


def _cursor_file(session_id: str, conv_key: str) -> Path:
    """Path to the read-position cursor for a session in a conversation."""
    return DM_CURSORS_DIR / f"{session_id}_{conv_key}.cursor"


def _blocks_file(session_id: str) -> Path:
    """Path to the block list for a session."""
    return DM_BLOCKS_DIR / f"{session_id}.json"


def _read_cursor(session_id: str, conv_key: str) -> int:
    """Read the cursor position for a session in a conversation."""
    path = _cursor_file(session_id, conv_key)
    try:
        return int(path.read_text("utf-8").strip())
    except (FileNotFoundError, ValueError):
        return 0


def _write_cursor(session_id: str, conv_key: str, position: int) -> None:
    """Write the cursor position for a session in a conversation."""
    path = _cursor_file(session_id, conv_key)
    path.write_text(str(position))


def _load_blocks(session_id: str) -> dict:
    """Load the block list for a session."""
    path = _blocks_file(session_id)
    try:
        return json.loads(path.read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError):
        return {"session_id": session_id, "blocked": {}}


def _save_blocks(session_id: str, blocks: dict) -> None:
    """Save the block list for a session."""
    _write_json_sync(_blocks_file(session_id), blocks)


def _all_conversation_files() -> list[Path]:
    """Return all conversation JSONL files in the DMs directory."""
    return list(DMS_DIR.glob("*.jsonl"))


# ── Send DM ────────────────────────────────────────────────────────────────


def send_dm(
    to_session_id: str,
    message: str,
    msg_type: str = "info",
    sender_id: Optional[str] = None,
    sender_name: Optional[str] = None,
    sender_platform: Optional[str] = None,
) -> dict:
    """Send a private message from the current session to another agent.

    Args:
        to_session_id: The recipient's session ID.
        message: The message text.
        msg_type: Message type (info, request, response, alert, ack, data).
        sender_id: Override sender ID (defaults to current session).
        sender_name: Override sender name.
        sender_platform: Override sender platform.

    Returns:
        The sent message dict, or an error dict.
    """
    sid = sender_id or SESSION_ID
    sname = sender_name or SESSION_NAME
    splatform = sender_platform or PLATFORM

    # Validate
    if not to_session_id or not to_session_id.strip():
        return {"error": "to_session_id is required"}
    if not message or not message.strip():
        return {"error": "message cannot be empty"}
    if msg_type not in VALID_MSG_TYPES:
        return {"error": f"invalid msg_type '{msg_type}', must be one of {VALID_MSG_TYPES}"}
    if to_session_id == sid:
        return {"error": "cannot send DM to yourself"}

    # Check if sender is blocked by recipient
    if is_blocked(sid, blocker_id=to_session_id):
        return {"error": f"you are blocked by {to_session_id}"}

    conv_key = _conversation_key(sid, to_session_id)
    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "conversation_key": conv_key,
        "from_session_id": sid,
        "from_session_name": sname,
        "from_platform": splatform,
        "to_session_id": to_session_id,
        "msg_type": msg_type,
        "message": message,
    }

    _append_jsonl_sync(_conversation_file(conv_key), entry)
    return entry


# ── Read DMs ───────────────────────────────────────────────────────────────


def read_dms(
    from_session_id: Optional[str] = None,
    unread_only: bool = True,
    reader_id: Optional[str] = None,
) -> dict:
    """Read DMs. If from_session_id is given, reads that conversation only.

    Args:
        from_session_id: Filter to messages from this specific agent.
        unread_only: If True, only return unread messages (based on cursor).
        reader_id: Override the reader session ID (defaults to current session).

    Returns:
        Dict with messages list and metadata.
    """
    rid = reader_id or SESSION_ID

    if from_session_id:
        # Read a specific conversation
        conv_key = _conversation_key(rid, from_session_id)
        conv_file = _conversation_file(conv_key)
        cursor = _read_cursor(rid, conv_key) if unread_only else 0
        messages = _read_jsonl_sync(conv_file, offset=cursor)

        # Filter to only messages TO this reader (skip own messages from cursor)
        # Actually, return all messages in the conversation from the offset
        total = len(_read_jsonl_sync(conv_file))
        new_cursor = total

        # Advance cursor
        if messages:
            _write_cursor(rid, conv_key, new_cursor)

        return {
            "conversation_key": conv_key,
            "partner_id": from_session_id,
            "messages": messages,
            "count": len(messages),
            "cursor": new_cursor,
        }

    # Read all conversations
    all_messages = []
    for conv_file in _all_conversation_files():
        conv_key = conv_file.stem
        all_entries = _read_jsonl_sync(conv_file)

        # Check if this reader is part of this conversation
        participant = False
        for entry in all_entries:
            if entry.get("from_session_id") == rid or entry.get("to_session_id") == rid:
                participant = True
                break

        if not participant:
            continue

        cursor = _read_cursor(rid, conv_key) if unread_only else 0
        messages = all_entries[cursor:]
        if messages:
            all_messages.extend(messages)
            if unread_only:
                _write_cursor(rid, conv_key, len(all_entries))

    # Sort all messages chronologically
    all_messages.sort(key=lambda m: m.get("timestamp", ""))

    return {
        "messages": all_messages,
        "count": len(all_messages),
    }


# ── Conversation List ──────────────────────────────────────────────────────


def list_conversations(session_id: Optional[str] = None) -> dict:
    """List all conversations the session is part of, with unread counts.

    Args:
        session_id: Override session ID (defaults to current session).

    Returns:
        Dict with conversations list.
    """
    sid = session_id or SESSION_ID
    conversations = []

    for conv_file in _all_conversation_files():
        conv_key = conv_file.stem
        all_entries = _read_jsonl_sync(conv_file)

        if not all_entries:
            continue

        # Check if this session is a participant
        participant = False
        partner_id = None
        for entry in all_entries:
            if entry.get("from_session_id") == sid:
                participant = True
                partner_id = entry.get("to_session_id")
            elif entry.get("to_session_id") == sid:
                participant = True
                partner_id = entry.get("from_session_id")
            if participant and partner_id:
                break

        if not participant:
            continue

        cursor = _read_cursor(sid, conv_key)
        total = len(all_entries)
        unread = max(0, total - cursor)
        last_msg = all_entries[-1]

        conversations.append({
            "conversation_key": conv_key,
            "partner_id": partner_id,
            "total_messages": total,
            "unread_count": unread,
            "last_message": last_msg.get("message", "")[:100],
            "last_timestamp": last_msg.get("timestamp", ""),
            "last_sender": last_msg.get("from_session_id", ""),
        })

    # Sort by most recent activity
    conversations.sort(key=lambda c: c.get("last_timestamp", ""), reverse=True)

    return {
        "session_id": sid,
        "conversations": conversations,
        "total_conversations": len(conversations),
    }


# ── Conversation History ──────────────────────────────────────────────────


def get_conversation(
    partner_id: str,
    limit: int = 50,
    before: Optional[str] = None,
    session_id: Optional[str] = None,
) -> dict:
    """Get paginated message history for a conversation.

    Args:
        partner_id: The other agent's session ID.
        limit: Max messages to return (default 50).
        before: ISO timestamp — only return messages before this time.
        session_id: Override session ID (defaults to current session).

    Returns:
        Dict with messages and pagination info.
    """
    sid = session_id or SESSION_ID
    conv_key = _conversation_key(sid, partner_id)
    conv_file = _conversation_file(conv_key)
    all_entries = _read_jsonl_sync(conv_file)

    if before:
        all_entries = [
            e for e in all_entries
            if e.get("timestamp", "") < before
        ]

    # Take the last `limit` messages (most recent)
    total = len(all_entries)
    page = all_entries[-limit:] if limit < total else all_entries
    has_more = total > limit

    return {
        "conversation_key": conv_key,
        "partner_id": partner_id,
        "messages": page,
        "count": len(page),
        "total": total,
        "has_more": has_more,
    }


# ── Message Status ─────────────────────────────────────────────────────────


def mark_read(
    partner_id: str,
    session_id: Optional[str] = None,
) -> dict:
    """Mark all messages in a conversation as read.

    Args:
        partner_id: The other agent's session ID.
        session_id: Override session ID (defaults to current session).

    Returns:
        Dict with the updated cursor position.
    """
    sid = session_id or SESSION_ID
    conv_key = _conversation_key(sid, partner_id)
    conv_file = _conversation_file(conv_key)
    all_entries = _read_jsonl_sync(conv_file)
    total = len(all_entries)

    _write_cursor(sid, conv_key, total)

    return {
        "conversation_key": conv_key,
        "partner_id": partner_id,
        "cursor": total,
        "status": "marked_read",
    }


def get_unread_count(session_id: Optional[str] = None) -> dict:
    """Return total unread DM count across all conversations.

    Args:
        session_id: Override session ID (defaults to current session).

    Returns:
        Dict with total unread count and per-conversation breakdown.
    """
    sid = session_id or SESSION_ID
    total_unread = 0
    breakdown = []

    for conv_file in _all_conversation_files():
        conv_key = conv_file.stem
        all_entries = _read_jsonl_sync(conv_file)

        if not all_entries:
            continue

        # Check if this session is a participant
        participant = False
        partner_id = None
        for entry in all_entries:
            if entry.get("from_session_id") == sid:
                participant = True
                partner_id = entry.get("to_session_id")
            elif entry.get("to_session_id") == sid:
                participant = True
                partner_id = entry.get("from_session_id")
            if participant and partner_id:
                break

        if not participant:
            continue

        cursor = _read_cursor(sid, conv_key)
        unread = max(0, len(all_entries) - cursor)
        if unread > 0:
            total_unread += unread
            breakdown.append({
                "conversation_key": conv_key,
                "partner_id": partner_id,
                "unread": unread,
            })

    return {
        "session_id": sid,
        "total_unread": total_unread,
        "breakdown": breakdown,
    }


# ── Block / Unblock ───────────────────────────────────────────────────────


def block_agent(
    target_session_id: str,
    reason: str = "",
    blocker_id: Optional[str] = None,
) -> dict:
    """Block an agent from sending DMs.

    Args:
        target_session_id: The agent to block.
        reason: Optional reason for blocking.
        blocker_id: Override blocker ID (defaults to current session).

    Returns:
        Confirmation dict.
    """
    bid = blocker_id or SESSION_ID

    if target_session_id == bid:
        return {"error": "cannot block yourself"}

    blocks = _load_blocks(bid)
    blocks.setdefault("blocked", {})
    blocks["blocked"][target_session_id] = {
        "reason": reason,
        "blocked_at": datetime.now(timezone.utc).isoformat(),
    }
    blocks["session_id"] = bid
    _save_blocks(bid, blocks)

    return {
        "status": "blocked",
        "target": target_session_id,
        "reason": reason,
    }


def unblock_agent(
    target_session_id: str,
    blocker_id: Optional[str] = None,
) -> dict:
    """Unblock an agent, restoring their ability to send DMs.

    Args:
        target_session_id: The agent to unblock.
        blocker_id: Override blocker ID (defaults to current session).

    Returns:
        Confirmation dict.
    """
    bid = blocker_id or SESSION_ID
    blocks = _load_blocks(bid)
    blocked = blocks.get("blocked", {})

    if target_session_id not in blocked:
        return {"error": f"{target_session_id} is not blocked"}

    del blocked[target_session_id]
    blocks["blocked"] = blocked
    _save_blocks(bid, blocks)

    return {
        "status": "unblocked",
        "target": target_session_id,
    }


def is_blocked(
    sender_id: str,
    blocker_id: Optional[str] = None,
) -> bool:
    """Check if sender_id is blocked by blocker_id.

    Args:
        sender_id: The agent to check.
        blocker_id: The agent who may have blocked them (defaults to current session).

    Returns:
        True if blocked, False otherwise.
    """
    bid = blocker_id or SESSION_ID
    blocks = _load_blocks(bid)
    return sender_id in blocks.get("blocked", {})


def list_blocked(session_id: Optional[str] = None) -> dict:
    """List all blocked agents for a session.

    Args:
        session_id: Override session ID (defaults to current session).

    Returns:
        Dict with blocked agents list.
    """
    sid = session_id or SESSION_ID
    blocks = _load_blocks(sid)
    blocked = blocks.get("blocked", {})

    agents = []
    for target_id, info in blocked.items():
        agents.append({
            "session_id": target_id,
            "reason": info.get("reason", ""),
            "blocked_at": info.get("blocked_at", ""),
        })

    return {
        "session_id": sid,
        "blocked_agents": agents,
        "count": len(agents),
    }


# ── Broadcast DM ──────────────────────────────────────────────────────────


def broadcast_dm(
    session_ids: list[str],
    message: str,
    msg_type: str = "info",
    sender_id: Optional[str] = None,
    sender_name: Optional[str] = None,
    sender_platform: Optional[str] = None,
) -> dict:
    """Send the same DM to multiple agents (individual conversations, not group chat).

    Args:
        session_ids: List of recipient session IDs.
        message: The message text.
        msg_type: Message type.
        sender_id: Override sender ID.
        sender_name: Override sender name.
        sender_platform: Override sender platform.

    Returns:
        Dict with results for each recipient.
    """
    if not session_ids:
        return {"error": "session_ids list is empty"}
    if not message or not message.strip():
        return {"error": "message cannot be empty"}

    results = []
    sent = 0
    failed = 0

    for target_id in session_ids:
        result = send_dm(
            to_session_id=target_id,
            message=message,
            msg_type=msg_type,
            sender_id=sender_id,
            sender_name=sender_name,
            sender_platform=sender_platform,
        )
        if "error" in result:
            failed += 1
            results.append({"to": target_id, "error": result["error"]})
        else:
            sent += 1
            results.append({"to": target_id, "message_id": result["id"]})

    return {
        "sent": sent,
        "failed": failed,
        "total": len(session_ids),
        "results": results,
    }
