# Chat Core — Starlark Backend (v0.2.0)
#
# Library package providing conversations, messages, participants,
# and read cursors. Consumable via lib.require("chat-core").
#
# Entry points:
#   on_request(req)  → REST API routes
#   Exported globals → create, send, history, add_participant,
#                      remove_participant, mark_read
#
# Modules: db, json, realtime


# ═══════════════════════════════════════════════
#  Helpers
# ═══════════════════════════════════════════════

def _resp(status, data):
    return {"status": status, "body": json.encode(data), "headers": {"Content-Type": "application/json"}}

def _str(v):
    if v == None:
        return ""
    return str(v)

def _int(v):
    if v == None:
        return 0
    s = str(v)
    if not s:
        return 0
    return int(s)

def _is_participant(conversation_id, user_id):
    """Check if user is a participant in the conversation."""
    rows = db.query("participants", filters={"conversation_id": conversation_id, "participant_id": user_id}, limit=1)
    return len(rows or []) > 0

def _get_participant(conversation_id, user_id):
    """Get participant record or None."""
    rows = db.query("participants", filters={"conversation_id": conversation_id, "participant_id": user_id}, limit=1)
    if rows and len(rows) > 0:
        return rows[0]
    return None

def _is_admin(conversation_id, user_id):
    """Check if user has admin role in the conversation."""
    p = _get_participant(conversation_id, user_id)
    if p:
        return _str(p.get("role", "")) == "admin"
    return False

def _get_conversation(conversation_id):
    """Get conversation by ID or None."""
    rows = db.query("conversations", filters={"id": conversation_id}, limit=1)
    if rows and len(rows) > 0:
        return rows[0]
    return None

def _now():
    """Current timestamp placeholder — db auto-populates created_at."""
    return ""


# ═══════════════════════════════════════════════
#  Exported API (lib.require("chat-core"))
# ═══════════════════════════════════════════════

def create(title, type="group", participants=None, creator_id="", creator_display_name=""):
    """Create a conversation and add initial participants.

    Args:
        title: conversation title
        type: "direct" or "group" (default "group")
        participants: list of dicts with {id, type?, display_name?, role?}
        creator_id: user ID of the creator (added as admin)
        creator_display_name: display name of the creator

    Returns:
        dict with conversation data
    """
    if type not in ("direct", "group"):
        type = "group"

    conv = db.insert("conversations", {
        "title": _str(title),
        "type": type,
        "created_by": _str(creator_id),
        "updated_at": "",
    })
    cid = conv["id"]

    # Add creator as admin participant
    if creator_id:
        db.insert("participants", {
            "conversation_id": cid,
            "participant_id": _str(creator_id),
            "participant_type": "user",
            "display_name": _str(creator_display_name),
            "role": "admin",
            "joined_at": "",
        })

    # Add initial participants
    for p in (participants or []):
        pid = _str(p.get("id", ""))
        if not pid or pid == _str(creator_id):
            continue
        db.insert("participants", {
            "conversation_id": cid,
            "participant_id": pid,
            "participant_type": _str(p.get("type", "user")),
            "display_name": _str(p.get("display_name", "")),
            "role": _str(p.get("role", "member")),
            "joined_at": "",
        })

    return conv


def send(conversation_id, participant_id, content, content_type="text"):
    """Send a message to a conversation.

    Args:
        conversation_id: target conversation
        participant_id: sender ID
        content: message content
        content_type: "text", "system", or "file" (default "text")

    Returns:
        dict with message data
    """
    if content_type not in ("text", "system", "file"):
        content_type = "text"

    msg = db.insert("messages", {
        "conversation_id": _str(conversation_id),
        "participant_id": _str(participant_id),
        "content": _str(content),
        "content_type": content_type,
        "edited_at": "",
    })

    # Update conversation timestamp
    db.update("conversations", _str(conversation_id), {"updated_at": msg.get("created_at", "")})

    # Publish realtime event
    realtime.publish(
        "conversation:" + _str(conversation_id),
        "message",
        {
            "id": msg.get("id", ""),
            "conversation_id": _str(conversation_id),
            "participant_id": _str(participant_id),
            "content": _str(content),
            "content_type": content_type,
            "created_at": msg.get("created_at", ""),
        },
    )

    return msg


def history(conversation_id, limit=50, cursor=""):
    """Get paginated message history for a conversation.

    Args:
        conversation_id: target conversation
        limit: max messages to return (1-100, default 50)
        cursor: created_at value of last message from previous page

    Returns:
        dict with {messages, has_more, next_cursor}
    """
    lim = _int(limit)
    if lim < 1 or lim > 100:
        lim = 50

    filters = {"conversation_id": _str(conversation_id)}
    before = {}
    if cursor:
        before = {"created_at": _str(cursor)}

    rows = db.query("messages", filters=filters, order="-created_at", limit=lim + 1, before=before)
    rows = rows or []

    has_more = len(rows) > lim
    if has_more:
        rows = rows[:lim]

    next_cursor = ""
    if has_more and rows:
        next_cursor = _str(rows[-1].get("created_at", ""))

    return {"messages": rows, "has_more": has_more, "next_cursor": next_cursor}


def add_participant(conversation_id, participant_id, participant_type="user", display_name="", role="member"):
    """Add a participant to a conversation.

    Returns:
        dict with participant data
    """
    cid = _str(conversation_id)
    pid = _str(participant_id)

    # Check if already a participant
    existing = db.query("participants", filters={"conversation_id": cid, "participant_id": pid}, limit=1)
    if existing and len(existing) > 0:
        return existing[0]

    p = db.insert("participants", {
        "conversation_id": cid,
        "participant_id": pid,
        "participant_type": _str(participant_type),
        "display_name": _str(display_name),
        "role": _str(role),
        "joined_at": "",
    })

    # Publish realtime event
    realtime.publish(
        "conversation:" + cid,
        "participant.added",
        {
            "conversation_id": cid,
            "participant_id": pid,
            "participant_type": _str(participant_type),
            "display_name": _str(display_name),
            "role": _str(role),
        },
    )

    # Insert system message
    send(cid, pid, "joined the conversation", "system")

    return p


def remove_participant(conversation_id, participant_id):
    """Remove a participant from a conversation.

    Returns:
        True on success
    """
    cid = _str(conversation_id)
    pid = _str(participant_id)

    # Delete participant record
    rows = db.query("participants", filters={"conversation_id": cid, "participant_id": pid}, limit=1)
    for r in (rows or []):
        db.delete("participants", r["id"])

    # Delete read cursor
    cursors = db.query("read_cursors", filters={"conversation_id": cid, "participant_id": pid}, limit=1)
    for c in (cursors or []):
        db.delete("read_cursors", c["id"])

    # Publish realtime event
    realtime.publish(
        "conversation:" + cid,
        "participant.removed",
        {"conversation_id": cid, "participant_id": pid},
    )

    return True


def mark_read(conversation_id, participant_id, last_read_message_id):
    """Update the read cursor for a participant.

    Returns:
        True on success
    """
    cid = _str(conversation_id)
    pid = _str(participant_id)
    mid = _str(last_read_message_id)

    # Delete existing cursor (upsert via delete+insert)
    existing = db.query("read_cursors", filters={"conversation_id": cid, "participant_id": pid}, limit=1)
    for r in (existing or []):
        db.delete("read_cursors", r["id"])

    db.insert("read_cursors", {
        "conversation_id": cid,
        "participant_id": pid,
        "last_read_message_id": mid,
    })

    return True


# ═══════════════════════════════════════════════
#  REST API dispatcher
# ═══════════════════════════════════════════════

def on_request(req):
    path   = req["path"]
    method = req["method"]
    user_id = req.get("user_id", "")

    # ── Conversations ──────────────────────────

    # GET /conversations — list user's conversations
    if method == "GET" and path == "/conversations":
        return _handle_list_conversations(req, user_id)

    # POST /conversations — create
    if method == "POST" and path == "/conversations":
        return _handle_create_conversation(req, user_id)

    # GET /search?q=term — search conversations and messages
    if method == "GET" and path == "/search":
        return _handle_search(req, user_id)

    # GET /unread — unread counts
    if method == "GET" and path == "/unread":
        return _handle_unread(user_id)

    # GET /conversations/:id
    if method == "GET" and path.startswith("/conversations/"):
        cid = path[len("/conversations/"):]
        return _handle_get_conversation(cid, user_id)

    # PUT /conversations/:id
    if method == "PUT" and path.startswith("/conversations/"):
        cid = path[len("/conversations/"):]
        return _handle_update_conversation(cid, req, user_id)

    # DELETE /conversations/:id
    if method == "DELETE" and path.startswith("/conversations/"):
        cid = path[len("/conversations/"):]
        return _handle_delete_conversation(cid, user_id)

    # ── Messages ───────────────────────────────

    # Routes: /messages/:conversation_id or /messages/:conversation_id/:message_id
    if path.startswith("/messages/"):
        remainder = path[len("/messages/"):]
        parts = remainder.split("/")
        cid = parts[0]
        mid = parts[1] if len(parts) > 1 else ""

        if method == "GET" and not mid:
            return _handle_list_messages(cid, req, user_id)
        if method == "POST" and not mid:
            return _handle_send_message(cid, req, user_id)
        if method == "PUT" and mid:
            return _handle_edit_message(cid, mid, req, user_id)
        if method == "DELETE" and mid:
            return _handle_delete_message(cid, mid, user_id)

    # ── Participants ───────────────────────────

    if path.startswith("/participants/"):
        remainder = path[len("/participants/"):]
        parts = remainder.split("/")
        cid = parts[0]
        pid = parts[1] if len(parts) > 1 else ""

        if method == "GET" and not pid:
            return _handle_list_participants(cid, user_id)
        if method == "POST" and not pid:
            return _handle_add_participant(cid, req, user_id)
        if method == "DELETE" and pid:
            return _handle_remove_participant(cid, pid, user_id)

    # ── Read cursors ──────────────────────────

    if method == "POST" and path.startswith("/read/"):
        cid = path[len("/read/"):]
        return _handle_mark_read(cid, req, user_id)

    return _resp(404, {"error": "not found"})


# ═══════════════════════════════════════════════
#  Conversation handlers
# ═══════════════════════════════════════════════

def _handle_list_conversations(req, user_id):
    """List conversations the user is a participant in."""
    # Get all conversation IDs for this user
    my_parts = db.query("participants", filters={"participant_id": user_id}, limit=500)
    if not my_parts:
        return _resp(200, {"data": []})

    cids = []
    for p in my_parts:
        cids.append(_str(p.get("conversation_id", "")))

    # Fetch conversations and enrich with last message
    items = []
    for cid in cids:
        conv = _get_conversation(cid)
        if not conv:
            continue

        # Get last message
        last_msgs = db.query("messages", filters={"conversation_id": cid}, order="-created_at", limit=1)
        last_msg = None
        if last_msgs and len(last_msgs) > 0:
            last_msg = {
                "id": last_msgs[0].get("id", ""),
                "content": _str(last_msgs[0].get("content", "")),
                "participant_id": _str(last_msgs[0].get("participant_id", "")),
                "content_type": _str(last_msgs[0].get("content_type", "")),
                "created_at": _str(last_msgs[0].get("created_at", "")),
            }

        # Get participant count
        parts = db.query("participants", filters={"conversation_id": cid}, limit=500)
        part_count = len(parts or [])

        items.append({
            "id": conv.get("id", ""),
            "title": conv.get("title", ""),
            "type": conv.get("type", ""),
            "created_by": conv.get("created_by", ""),
            "updated_at": conv.get("updated_at", ""),
            "created_at": conv.get("created_at", ""),
            "last_message": last_msg,
            "participant_count": part_count,
        })

    # Sort by updated_at descending (most recent first)
    items = sorted(items, key=lambda x: x.get("updated_at", "") or x.get("created_at", ""), reverse=True)

    return _resp(200, {"data": items})


def _handle_create_conversation(req, user_id):
    """Create a new conversation."""
    body = json.decode(req.get("body", "{}"))
    title = _str(body.get("title", ""))
    conv_type = _str(body.get("type", "group"))
    participants_list = body.get("participants", [])
    creator_name = _str(body.get("creator_display_name", ""))

    conv = create(title, conv_type, participants_list, user_id, creator_name)
    return _resp(201, conv)


def _handle_get_conversation(cid, user_id):
    """Get conversation detail with participants."""
    if not _is_participant(cid, user_id):
        return _resp(403, {"error": "not a participant"})

    conv = _get_conversation(cid)
    if not conv:
        return _resp(404, {"error": "conversation not found"})

    parts = db.query("participants", filters={"conversation_id": cid}, limit=500)

    return _resp(200, {
        "id": conv.get("id", ""),
        "title": conv.get("title", ""),
        "type": conv.get("type", ""),
        "created_by": conv.get("created_by", ""),
        "updated_at": conv.get("updated_at", ""),
        "created_at": conv.get("created_at", ""),
        "participants": parts or [],
    })


def _handle_update_conversation(cid, req, user_id):
    """Update conversation (admin only)."""
    if not _is_admin(cid, user_id):
        return _resp(403, {"error": "admin only"})

    body = json.decode(req.get("body", "{}"))
    patch = {}
    title = body.get("title", None)
    if title != None:
        patch["title"] = _str(title)

    if patch:
        db.update("conversations", cid, patch)

    conv = _get_conversation(cid)
    return _resp(200, conv)


def _handle_delete_conversation(cid, user_id):
    """Delete conversation and all associated data (admin only)."""
    if not _is_admin(cid, user_id):
        return _resp(403, {"error": "admin only"})

    # Cascade delete: messages, participants, read_cursors, then conversation
    msgs = db.query("messages", filters={"conversation_id": cid}, limit=5000)
    for m in (msgs or []):
        db.delete("messages", m["id"])

    parts = db.query("participants", filters={"conversation_id": cid}, limit=500)
    for p in (parts or []):
        db.delete("participants", p["id"])

    cursors = db.query("read_cursors", filters={"conversation_id": cid}, limit=500)
    for c in (cursors or []):
        db.delete("read_cursors", c["id"])

    db.delete("conversations", cid)
    return _resp(200, {"ok": True})


# ═══════════════════════════════════════════════
#  Message handlers
# ═══════════════════════════════════════════════

def _handle_list_messages(cid, req, user_id):
    """Paginated message history."""
    if not _is_participant(cid, user_id):
        return _resp(403, {"error": "not a participant"})

    q = req.get("query", {})
    limit_val = _int(q.get("limit", "50"))
    cursor = _str(q.get("cursor", ""))

    result = history(cid, limit_val, cursor)
    return _resp(200, result)


def _handle_send_message(cid, req, user_id):
    """Send a message."""
    if not _is_participant(cid, user_id):
        return _resp(403, {"error": "not a participant"})

    body = json.decode(req.get("body", "{}"))
    content = _str(body.get("content", ""))
    content_type = _str(body.get("content_type", "text"))

    if not content:
        return _resp(400, {"error": "content required"})

    msg = send(cid, user_id, content, content_type)
    return _resp(201, msg)


def _handle_edit_message(cid, mid, req, user_id):
    """Edit a message (author only)."""
    if not _is_participant(cid, user_id):
        return _resp(403, {"error": "not a participant"})

    # Verify ownership
    msgs = db.query("messages", filters={"id": mid}, limit=1)
    if not msgs:
        return _resp(404, {"error": "message not found"})

    msg = msgs[0]
    if _str(msg.get("participant_id", "")) != user_id:
        return _resp(403, {"error": "can only edit own messages"})

    body = json.decode(req.get("body", "{}"))
    content = _str(body.get("content", ""))
    if not content:
        return _resp(400, {"error": "content required"})

    # Use created_at of the message as the edited_at marker
    db.update("messages", mid, {"content": content, "edited_at": msg.get("created_at", "")})

    # Publish edit event
    realtime.publish(
        "conversation:" + cid,
        "message.edited",
        {"id": mid, "conversation_id": cid, "content": content, "edited_by": user_id},
    )

    updated = db.query("messages", filters={"id": mid}, limit=1)
    return _resp(200, updated[0] if updated else {})


def _handle_delete_message(cid, mid, user_id):
    """Delete a message (author or admin)."""
    if not _is_participant(cid, user_id):
        return _resp(403, {"error": "not a participant"})

    msgs = db.query("messages", filters={"id": mid}, limit=1)
    if not msgs:
        return _resp(404, {"error": "message not found"})

    msg = msgs[0]
    is_author = _str(msg.get("participant_id", "")) == user_id
    is_conv_admin = _is_admin(cid, user_id)

    if not is_author and not is_conv_admin:
        return _resp(403, {"error": "can only delete own messages or be admin"})

    db.delete("messages", mid)

    # Publish delete event
    realtime.publish(
        "conversation:" + cid,
        "message.deleted",
        {"id": mid, "conversation_id": cid, "deleted_by": user_id},
    )

    return _resp(200, {"ok": True})


# ═══════════════════════════════════════════════
#  Participant handlers
# ═══════════════════════════════════════════════

def _handle_list_participants(cid, user_id):
    """List participants in a conversation."""
    if not _is_participant(cid, user_id):
        return _resp(403, {"error": "not a participant"})

    parts = db.query("participants", filters={"conversation_id": cid}, limit=500)
    return _resp(200, {"data": parts or []})


def _handle_add_participant(cid, req, user_id):
    """Add a participant (admin only)."""
    if not _is_admin(cid, user_id):
        return _resp(403, {"error": "admin only"})

    body = json.decode(req.get("body", "{}"))
    pid = _str(body.get("participant_id", ""))
    if not pid:
        return _resp(400, {"error": "participant_id required"})

    ptype = _str(body.get("participant_type", "user"))
    display_name = _str(body.get("display_name", ""))
    role = _str(body.get("role", "member"))

    p = add_participant(cid, pid, ptype, display_name, role)
    return _resp(201, p)


def _handle_remove_participant(cid, pid, user_id):
    """Remove a participant (admin only, or self-remove)."""
    is_self = pid == user_id
    if not is_self and not _is_admin(cid, user_id):
        return _resp(403, {"error": "admin only (or remove yourself)"})

    remove_participant(cid, pid)
    return _resp(200, {"ok": True})


# ═══════════════════════════════════════════════
#  Read cursor handlers
# ═══════════════════════════════════════════════

def _handle_mark_read(cid, req, user_id):
    """Mark conversation as read up to a message."""
    if not _is_participant(cid, user_id):
        return _resp(403, {"error": "not a participant"})

    body = json.decode(req.get("body", "{}"))
    mid = _str(body.get("last_read_message_id", ""))
    if not mid:
        return _resp(400, {"error": "last_read_message_id required"})

    mark_read(cid, user_id, mid)
    return _resp(200, {"ok": True})


def _handle_search(req, user_id):
    """Search across conversation titles and message content."""
    q = _str(req.get("query", {}).get("q", ""))
    if len(q) < 2:
        return _resp(400, {"error": "query must be at least 2 characters"})

    pattern = "%" + q + "%"

    # Get user's conversation IDs
    my_parts = db.query("participants", filters={"participant_id": user_id}, limit=500)
    if not my_parts:
        return _resp(200, {"conversations": [], "messages": []})

    cid_set = {}
    for p in my_parts:
        cid_set[_str(p.get("conversation_id", ""))] = True

    # Search conversations by title
    matching_convs = db.query("conversations", search_like={"title": pattern}, limit=50)
    conv_results = []
    for conv in (matching_convs or []):
        cid = _str(conv.get("id", ""))
        if cid in cid_set:
            conv_results.append({
                "id": cid,
                "title": conv.get("title", ""),
                "type": conv.get("type", ""),
                "updated_at": conv.get("updated_at", ""),
                "created_at": conv.get("created_at", ""),
            })

    # Search messages in user's conversations
    msg_results = []
    for cid in cid_set:
        if not cid:
            continue
        msgs = db.query("messages", filters={"conversation_id": cid}, search_like={"content": pattern}, order="-created_at", limit=5)
        for m in (msgs or []):
            msg_results.append({
                "id": m.get("id", ""),
                "conversation_id": cid,
                "participant_id": m.get("participant_id", ""),
                "content": _str(m.get("content", "")),
                "content_type": _str(m.get("content_type", "")),
                "created_at": _str(m.get("created_at", "")),
            })

    # Sort messages by created_at descending, limit to 50
    msg_results = sorted(msg_results, key=lambda x: x.get("created_at", ""), reverse=True)[:50]

    return _resp(200, {"conversations": conv_results, "messages": msg_results})


def _handle_unread(user_id):
    """Get unread counts for all user's conversations."""
    my_parts = db.query("participants", filters={"participant_id": user_id}, limit=500)
    if not my_parts:
        return _resp(200, {"data": {}})

    counts = {}
    for p in my_parts:
        cid = _str(p.get("conversation_id", ""))
        if not cid:
            continue

        # Get read cursor
        cursors = db.query("read_cursors", filters={"conversation_id": cid, "participant_id": user_id}, limit=1)

        if cursors and len(cursors) > 0:
            last_read_id = _str(cursors[0].get("last_read_message_id", ""))
            if last_read_id:
                # Get the created_at of the last read message
                last_read_msgs = db.query("messages", filters={"id": last_read_id}, limit=1)
                if last_read_msgs and len(last_read_msgs) > 0:
                    last_read_at = _str(last_read_msgs[0].get("created_at", ""))
                    # Count messages after the read cursor
                    unread = db.query("messages", filters={"conversation_id": cid}, after={"created_at": last_read_at}, limit=1000)
                    counts[cid] = len(unread or [])
                else:
                    # Last read message was deleted — count all
                    all_msgs = db.query("messages", filters={"conversation_id": cid}, limit=1000)
                    counts[cid] = len(all_msgs or [])
            else:
                # No cursor value — all messages are unread
                all_msgs = db.query("messages", filters={"conversation_id": cid}, limit=1000)
                counts[cid] = len(all_msgs or [])
        else:
            # No cursor at all — all messages are unread
            all_msgs = db.query("messages", filters={"conversation_id": cid}, limit=1000)
            counts[cid] = len(all_msgs or [])

    return _resp(200, {"data": counts})
