"""Discord bot configuration REST API routes."""

from __future__ import annotations

import json

from fastapi import APIRouter, Depends, HTTPException, Query, Request

from peon_mcp.common.auth import AuthInfo, get_auth
from peon_mcp.common.dependencies import get_db, get_project_id
from peon_mcp.common.scoping import authorize_project_access
from peon_mcp.common.schemas import PaginatedResponse
from peon_mcp.db import row_to_dict
from peon_mcp.discord.schemas import (
    ActivityLogCreate,
    ActivityLogResponse,
    BotStatusResponse,
    ChannelMappingCreate,
    ChannelMappingResponse,
    ChannelMappingUpdate,
    DiscordConfigCreate,
    DiscordConfigResponse,
    DiscordConfigUpdate,
    DiscordInboundMessageResponse,
    InboundMessageAck,
    InboundMessageCreate,
    InboundMessageResponse,
    PairedUserCreate,
    PairedUserResponse,
    SendMessageRequest,
    SendMessageResponse,
)

router = APIRouter(tags=["Discord"])


def _mask_token(token: str) -> str:
    """Return a masked version of a bot token for safe display."""
    if not token:
        return ""
    if len(token) <= 8:
        return "***"
    return token[:4] + "***" + token[-4:]


def _row_to_config_response(row: dict) -> dict:
    """Convert a raw discord_configs row to a DiscordConfigResponse-compatible dict."""
    d = dict(row)
    d["bot_token_masked"] = _mask_token(d.pop("bot_token", ""))
    d["enabled"] = bool(d.get("enabled", 1))
    d["require_mention"] = bool(d.get("require_mention", 0))
    return d


def _row_to_mapping_response(row: dict) -> dict:
    """Convert a raw discord_channel_mappings row to a ChannelMappingResponse-compatible dict."""
    d = dict(row)
    d["allowed_user_ids"] = json.loads(d.get("allowed_user_ids") or "[]")
    d["allowed_role_ids"] = json.loads(d.get("allowed_role_ids") or "[]")
    d["subscribed_events"] = json.loads(d.get("subscribed_events") or "[]")
    d["enabled"] = bool(d.get("enabled", 1))
    d.setdefault("project_id", "")
    return d


# ---------------------------------------------------------------------------
# Discord Configs
# ---------------------------------------------------------------------------


@router.get(
    "/api/discord/configs",
    response_model=PaginatedResponse[DiscordConfigResponse],
)
async def list_discord_configs(
    limit: int = Query(default=50, ge=1, le=200),
    offset: int = Query(default=0, ge=0),
    db=Depends(get_db),
):
    """List all Discord bot configurations (global, not project-scoped)."""
    count_rows = await db.execute_fetchall(
        "SELECT COUNT(*) FROM discord_configs",
    )
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_configs ORDER BY created_at DESC LIMIT ? OFFSET ?",
        (limit, offset),
    )

    total = count_rows[0]["count"] if count_rows else 0
    items = [_row_to_config_response(row_to_dict(r)) for r in rows]
    return {
        "items": items,
        "total": total,
        "limit": limit,
        "offset": offset,
        "has_next": (offset + len(items)) < total,
    }


@router.post(
    "/api/discord/configs",
    response_model=DiscordConfigResponse,
    status_code=201,
)
async def create_discord_config(body: DiscordConfigCreate, db=Depends(get_db)):
    """Create a new global Discord bot configuration."""
    dm_policy = body.dm_policy if body.dm_policy in ("open", "pairing", "disabled") else "disabled"
    guild_policy = body.guild_policy if body.guild_policy in ("open", "allowlist", "disabled") else "open"

    cursor = await db.execute(
        """INSERT INTO discord_configs
           (bot_token, guild_id, enabled, dm_policy, guild_policy, require_mention, history_limit, model, system_prompt)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
           RETURNING id""",
        (
            body.bot_token,
            body.guild_id,
            body.enabled,
            dm_policy,
            guild_policy,
            body.require_mention,
            body.history_limit,
            body.model,
            body.system_prompt,
        ),
    )
    new_id = cursor.lastrowid
    await db.commit()
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_configs WHERE id = ?", (new_id,)
    )
    return _row_to_config_response(row_to_dict(rows[0]))


@router.get("/api/discord/configs/{config_id}", response_model=DiscordConfigResponse)
async def get_discord_config(config_id: int, db=Depends(get_db)):
    """Get a single Discord bot configuration by ID."""
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Discord config not found")
    return _row_to_config_response(row_to_dict(rows[0]))


@router.put("/api/discord/configs/{config_id}", response_model=DiscordConfigResponse)
async def update_discord_config(
    config_id: int, body: DiscordConfigUpdate, db=Depends(get_db)
):
    """Update an existing Discord bot configuration."""
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Discord config not found")

    updates: list[str] = []
    params: list = []

    if body.bot_token is not None:
        updates.append("bot_token = ?")
        params.append(body.bot_token)
    if body.guild_id is not None:
        updates.append("guild_id = ?")
        params.append(body.guild_id)
    if body.enabled is not None:
        updates.append("enabled = ?")
        params.append(body.enabled)
    if body.dm_policy is not None:
        if body.dm_policy not in ("open", "pairing", "disabled"):
            raise HTTPException(400, detail="dm_policy must be open, pairing, or disabled")
        updates.append("dm_policy = ?")
        params.append(body.dm_policy)
    if body.guild_policy is not None:
        if body.guild_policy not in ("open", "allowlist", "disabled"):
            raise HTTPException(400, detail="guild_policy must be open, allowlist, or disabled")
        updates.append("guild_policy = ?")
        params.append(body.guild_policy)
    if body.require_mention is not None:
        updates.append("require_mention = ?")
        params.append(body.require_mention)
    if body.history_limit is not None:
        updates.append("history_limit = ?")
        params.append(body.history_limit)
    if body.model is not None:
        updates.append("model = ?")
        params.append(body.model)
    if body.system_prompt is not None:
        updates.append("system_prompt = ?")
        params.append(body.system_prompt)

    if updates:
        updates.append("updated_at = CURRENT_TIMESTAMP")
        params.append(config_id)
        await db.execute(
            f"UPDATE discord_configs SET {', '.join(updates)} WHERE id = ?",
            params,
        )
        await db.commit()

    rows = await db.execute_fetchall(
        "SELECT * FROM discord_configs WHERE id = ?", (config_id,)
    )
    return _row_to_config_response(row_to_dict(rows[0]))


@router.delete("/api/discord/configs/{config_id}", status_code=204)
async def delete_discord_config(config_id: int, db=Depends(get_db)):
    """Delete a Discord bot configuration and all its channel mappings."""
    rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Discord config not found")
    await db.execute("DELETE FROM discord_configs WHERE id = ?", (config_id,))
    await db.commit()


# ---------------------------------------------------------------------------
# Channel Mappings
# ---------------------------------------------------------------------------


@router.get(
    "/api/discord/configs/{config_id}/channels",
    response_model=list[ChannelMappingResponse],
)
async def list_channel_mappings(config_id: int, db=Depends(get_db)):
    """List all channel mappings for a Discord bot configuration."""
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_channel_mappings WHERE discord_config_id = ? ORDER BY created_at DESC",
        (config_id,),
    )
    return [_row_to_mapping_response(row_to_dict(r)) for r in rows]


@router.post(
    "/api/discord/configs/{config_id}/channels",
    response_model=ChannelMappingResponse,
    status_code=201,
)
async def create_channel_mapping(
    config_id: int, body: ChannelMappingCreate, db=Depends(get_db)
):
    """Add a channel mapping to a Discord bot configuration."""
    cfg_rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not cfg_rows:
        raise HTTPException(404, detail="Discord config not found")

    cursor = await db.execute(
        """INSERT INTO discord_channel_mappings
           (discord_config_id, channel_id, channel_name, allowed_user_ids, allowed_role_ids, subscribed_events, enabled, project_id)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)
           RETURNING id""",
        (
            config_id,
            body.channel_id,
            body.channel_name,
            json.dumps(body.allowed_user_ids),
            json.dumps(body.allowed_role_ids),
            json.dumps(body.subscribed_events),
            body.enabled,
            body.project_id,
        ),
    )
    new_id = cursor.lastrowid
    await db.commit()
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_channel_mappings WHERE id = ?", (new_id,)
    )
    return _row_to_mapping_response(row_to_dict(rows[0]))


@router.put(
    "/api/discord/configs/{config_id}/channels/{mapping_id}",
    response_model=ChannelMappingResponse,
)
async def update_channel_mapping(
    config_id: int, mapping_id: int, body: ChannelMappingUpdate, db=Depends(get_db)
):
    """Update a channel mapping."""
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_channel_mappings WHERE id = ? AND discord_config_id = ?",
        (mapping_id, config_id),
    )
    if not rows:
        raise HTTPException(404, detail="Channel mapping not found")

    updates: list[str] = []
    params: list = []

    if body.channel_name is not None:
        updates.append("channel_name = ?")
        params.append(body.channel_name)
    if body.allowed_user_ids is not None:
        updates.append("allowed_user_ids = ?")
        params.append(json.dumps(body.allowed_user_ids))
    if body.allowed_role_ids is not None:
        updates.append("allowed_role_ids = ?")
        params.append(json.dumps(body.allowed_role_ids))
    if body.subscribed_events is not None:
        updates.append("subscribed_events = ?")
        params.append(json.dumps(body.subscribed_events))
    if body.enabled is not None:
        updates.append("enabled = ?")
        params.append(body.enabled)
    if body.project_id is not None:
        updates.append("project_id = ?")
        params.append(body.project_id)

    if updates:
        params.extend([mapping_id, config_id])
        await db.execute(
            f"UPDATE discord_channel_mappings SET {', '.join(updates)} WHERE id = ? AND discord_config_id = ?",
            params,
        )
        await db.commit()

    rows = await db.execute_fetchall(
        "SELECT * FROM discord_channel_mappings WHERE id = ?", (mapping_id,)
    )
    return _row_to_mapping_response(row_to_dict(rows[0]))


@router.delete(
    "/api/discord/configs/{config_id}/channels/{mapping_id}",
    status_code=204,
)
async def delete_channel_mapping(
    config_id: int, mapping_id: int, db=Depends(get_db)
):
    """Remove a channel mapping."""
    rows = await db.execute_fetchall(
        "SELECT id FROM discord_channel_mappings WHERE id = ? AND discord_config_id = ?",
        (mapping_id, config_id),
    )
    if not rows:
        raise HTTPException(404, detail="Channel mapping not found")
    await db.execute(
        "DELETE FROM discord_channel_mappings WHERE id = ? AND discord_config_id = ?",
        (mapping_id, config_id),
    )
    await db.commit()


# ---------------------------------------------------------------------------
# Project-scoped channel mappings
# ---------------------------------------------------------------------------


@router.get(
    "/api/projects/{owner}/{repo}/discord/channels",
    response_model=list[ChannelMappingResponse],
)
async def list_project_channel_mappings(
    request: Request,
    project_id: str = Depends(get_project_id),
    auth: AuthInfo = Depends(get_auth),
    db=Depends(get_db),
):
    await authorize_project_access(db, auth, project_id, request)
    """List all channel mappings for a project (across all configs)."""
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_channel_mappings WHERE project_id = ? ORDER BY created_at DESC",
        (project_id,),
    )
    return [_row_to_mapping_response(row_to_dict(r)) for r in rows]


@router.post(
    "/api/projects/{owner}/{repo}/discord/channels",
    response_model=ChannelMappingResponse,
    status_code=201,
)
async def create_project_channel_mapping(
    body: ChannelMappingCreate,
    request: Request,
    project_id: str = Depends(get_project_id),
    auth: AuthInfo = Depends(get_auth),
    db=Depends(get_db),
):
    await authorize_project_access(db, auth, project_id, request)
    """Add a channel mapping scoped to a project.

    The body must include ``discord_config_id`` to specify which bot config
    owns this channel.
    """
    if not body.discord_config_id:
        raise HTTPException(400, detail="discord_config_id is required")

    cfg_rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (body.discord_config_id,)
    )
    if not cfg_rows:
        raise HTTPException(404, detail="Discord config not found")

    proj_rows = await db.execute_fetchall(
        "SELECT id FROM projects WHERE id = ?", (project_id,)
    )
    if not proj_rows:
        raise HTTPException(404, detail=f"Project '{project_id}' not found")

    cursor = await db.execute(
        """INSERT INTO discord_channel_mappings
           (discord_config_id, channel_id, channel_name, allowed_user_ids, allowed_role_ids, subscribed_events, enabled, project_id)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)
           RETURNING id""",
        (
            body.discord_config_id,
            body.channel_id,
            body.channel_name,
            json.dumps(body.allowed_user_ids),
            json.dumps(body.allowed_role_ids),
            json.dumps(body.subscribed_events),
            body.enabled,
            project_id,
        ),
    )
    new_id = cursor.lastrowid
    await db.commit()
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_channel_mappings WHERE id = ?", (new_id,)
    )
    return _row_to_mapping_response(row_to_dict(rows[0]))


@router.put(
    "/api/projects/{owner}/{repo}/discord/channels/{mapping_id}",
    response_model=ChannelMappingResponse,
)
async def update_project_channel_mapping(
    mapping_id: int,
    body: ChannelMappingUpdate,
    request: Request,
    project_id: str = Depends(get_project_id),
    auth: AuthInfo = Depends(get_auth),
    db=Depends(get_db),
):
    await authorize_project_access(db, auth, project_id, request)
    """Update a channel mapping scoped to a project."""
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_channel_mappings WHERE id = ? AND project_id = ?",
        (mapping_id, project_id),
    )
    if not rows:
        raise HTTPException(404, detail="Channel mapping not found")

    updates: list[str] = []
    params: list = []

    if body.channel_name is not None:
        updates.append("channel_name = ?")
        params.append(body.channel_name)
    if body.allowed_user_ids is not None:
        updates.append("allowed_user_ids = ?")
        params.append(json.dumps(body.allowed_user_ids))
    if body.allowed_role_ids is not None:
        updates.append("allowed_role_ids = ?")
        params.append(json.dumps(body.allowed_role_ids))
    if body.subscribed_events is not None:
        updates.append("subscribed_events = ?")
        params.append(json.dumps(body.subscribed_events))
    if body.enabled is not None:
        updates.append("enabled = ?")
        params.append(body.enabled)

    if updates:
        params.extend([mapping_id, project_id])
        await db.execute(
            f"UPDATE discord_channel_mappings SET {', '.join(updates)} WHERE id = ? AND project_id = ?",
            params,
        )
        await db.commit()

    rows = await db.execute_fetchall(
        "SELECT * FROM discord_channel_mappings WHERE id = ?", (mapping_id,)
    )
    return _row_to_mapping_response(row_to_dict(rows[0]))


@router.delete(
    "/api/projects/{owner}/{repo}/discord/channels/{mapping_id}",
    status_code=204,
)
async def delete_project_channel_mapping(
    mapping_id: int,
    request: Request,
    project_id: str = Depends(get_project_id),
    auth: AuthInfo = Depends(get_auth),
    db=Depends(get_db),
):
    await authorize_project_access(db, auth, project_id, request)
    """Remove a channel mapping scoped to a project."""
    rows = await db.execute_fetchall(
        "SELECT id FROM discord_channel_mappings WHERE id = ? AND project_id = ?",
        (mapping_id, project_id),
    )
    if not rows:
        raise HTTPException(404, detail="Channel mapping not found")
    await db.execute(
        "DELETE FROM discord_channel_mappings WHERE id = ? AND project_id = ?",
        (mapping_id, project_id),
    )
    await db.commit()


# ---------------------------------------------------------------------------
# Bot gateway control
# ---------------------------------------------------------------------------


def _get_bot_manager(request: Request):
    """Retrieve BotManager from app state; raise 503 if unavailable."""
    manager = getattr(request.app.state, "bot_manager", None)
    if manager is None:
        raise HTTPException(503, detail="Bot manager not initialised")
    return manager


@router.post(
    "/api/discord/configs/{config_id}/start",
    response_model=BotStatusResponse,
)
async def start_discord_bot(
    config_id: int,
    request: Request,
    db=Depends(get_db),
):
    """Start the Discord bot gateway for a given config.

    Fetches the config from the database and starts (or restarts) the
    corresponding :class:`DiscordGateway`.
    """
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Discord config not found")

    config = row_to_dict(rows[0])
    token = config.get("bot_token", "")
    if not token:
        raise HTTPException(400, detail="Discord config has no bot token")

    manager = _get_bot_manager(request)
    try:
        await manager.start_bot(config_id, token, config)
    except RuntimeError as exc:
        raise HTTPException(503, detail=str(exc)) from exc

    return BotStatusResponse(
        config_id=config_id,
        status=manager.get_bot_status(config_id),
        error=manager.get_bot_error(config_id),
    )


@router.post(
    "/api/discord/configs/{config_id}/stop",
    response_model=BotStatusResponse,
)
async def stop_discord_bot(
    config_id: int,
    request: Request,
    db=Depends(get_db),
):
    """Stop the Discord bot gateway for a given config."""
    rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Discord config not found")

    manager = _get_bot_manager(request)
    await manager.stop_bot(config_id)

    return BotStatusResponse(
        config_id=config_id,
        status="disconnected",
        error=None,
    )


@router.get(
    "/api/discord/configs/{config_id}/status",
    response_model=BotStatusResponse,
)
async def get_discord_bot_status(
    config_id: int,
    request: Request,
    db=Depends(get_db),
):
    """Return the current connection status of a Discord bot gateway."""
    rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Discord config not found")

    manager = _get_bot_manager(request)
    return BotStatusResponse(
        config_id=config_id,
        status=manager.get_bot_status(config_id),
        error=manager.get_bot_error(config_id),
    )


@router.post(
    "/api/discord/configs/{config_id}/send",
    response_model=SendMessageResponse,
)
async def send_discord_message(
    config_id: int,
    body: SendMessageRequest,
    request: Request,
    db=Depends(get_db),
):
    """Send a message to a Discord channel via the configured bot.

    Requires the bot gateway to be running.  Returns sent=True on success.
    """
    rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Discord config not found")

    manager = _get_bot_manager(request)
    sent = await manager.send_message(
        config_id=config_id,
        channel_id=body.channel_id,
        content=body.content,
        embed_title=body.embed_title,
        embed_description=body.embed_description,
    )

    error = None if sent else manager.get_bot_error(config_id) or "Bot not connected"
    return SendMessageResponse(
        config_id=config_id,
        channel_id=body.channel_id,
        sent=sent,
        error=error,
    )



# ---------------------------------------------------------------------------
# Inbound messages
# ---------------------------------------------------------------------------


def _row_to_message_response(row: dict) -> dict:
    """Convert a raw discord_messages row to an InboundMessageResponse-compatible dict."""
    d = dict(row)
    d["processed"] = bool(d.get("processed", 0))
    d["denied"] = bool(d.get("denied", 0))
    d["deny_reason"] = d.get("deny_reason", "") or ""
    return d


@router.post(
    "/api/discord/messages/inbound",
    response_model=InboundMessageAck,
    status_code=201,
)
async def receive_inbound_message(body: InboundMessageCreate, db=Depends(get_db)):
    """Accept an inbound Discord message from the bot gateway.

    Stores the message in discord_messages and returns an acknowledgment.
    The caller (gateway on_message handler) should POST here for both accepted
    and denied messages (with denied=True) to create a complete audit log.
    """
    cfg_rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (body.config_id,)
    )
    if not cfg_rows:
        raise HTTPException(404, detail="Discord config not found")

    cursor = await db.execute(
        """INSERT INTO discord_messages
           (config_id, channel_id, user_id, username, content, message_id,
            replied_to, processed, denied, deny_reason, member_roles)
           VALUES (?, ?, ?, ?, ?, ?, ?, FALSE, ?, ?, ?)
           RETURNING id""",
        (
            body.config_id,
            body.channel_id,
            body.user_id,
            body.username,
            body.content,
            body.message_id,
            body.replied_to,
            body.denied,
            body.deny_reason,
            json.dumps(body.member_roles),
        ),
    )
    new_id = cursor.lastrowid
    await db.commit()
    status = "denied" if body.denied else "queued"
    return {"message_db_id": new_id, "status": status}


@router.get(
    "/api/discord/messages/pending",
    response_model=list[InboundMessageResponse],
)
async def list_pending_messages(
    project_id: str | None = Query(default=None),
    limit: int = Query(default=50, ge=1, le=200),
    db=Depends(get_db),
):
    """List pending (unprocessed, non-denied) inbound Discord messages.

    Returns messages ordered oldest-first so the loop can process them in
    arrival order.
    """
    if project_id:
        rows = await db.execute_fetchall(
            """SELECT dm.* FROM discord_messages dm
               JOIN discord_channel_mappings dcm ON dcm.discord_config_id = dm.config_id AND dcm.channel_id = dm.channel_id
               WHERE dm.processed = FALSE AND dm.denied = FALSE AND dcm.project_id = ?
               ORDER BY dm.created_at ASC
               LIMIT ?""",
            (project_id, limit),
        )
    else:
        rows = await db.execute_fetchall(
            "SELECT * FROM discord_messages WHERE processed = FALSE AND denied = FALSE"
            " ORDER BY created_at ASC LIMIT ?",
            (limit,),
        )
    return [_row_to_message_response(row_to_dict(r)) for r in rows]


@router.post(
    "/api/discord/messages/{message_id}/ack",
    response_model=DiscordInboundMessageResponse,
)
async def ack_discord_message(
    message_id: int,
    status: str = "processed",
    db=Depends(get_db),
):
    """Acknowledge (mark as processed or ignored) an inbound Discord message.

    Args:
        message_id: ID of the inbound message.
        status: New status — 'processed' or 'ignored' (default: 'processed').
    """
    if status not in ("processed", "ignored"):
        raise HTTPException(400, detail="status must be 'processed' or 'ignored'")

    rows = await db.execute_fetchall(
        "SELECT * FROM discord_inbound_messages WHERE id = ?", (message_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Inbound message not found")

    await db.execute(
        "UPDATE discord_inbound_messages SET status = ? WHERE id = ?",
        (status, message_id),
    )
    await db.commit()

    rows = await db.execute_fetchall(
        "SELECT * FROM discord_inbound_messages WHERE id = ?", (message_id,)
    )
    return row_to_dict(rows[0])


@router.put(
    "/api/discord/messages/{message_id}/processed",
    response_model=InboundMessageResponse,
)
async def mark_message_processed(message_id: int, db=Depends(get_db)):
    """Mark an inbound Discord message as processed.

    Call this after the agent has consumed and acted on the message.
    """
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_messages WHERE id = ?", (message_id,)
    )
    if not rows:
        raise HTTPException(404, detail="Discord message not found")

    await db.execute(
        "UPDATE discord_messages SET processed = TRUE WHERE id = ?",
        (message_id,),
    )
    await db.commit()

    rows = await db.execute_fetchall(
        "SELECT * FROM discord_messages WHERE id = ?", (message_id,)
    )
    return _row_to_message_response(row_to_dict(rows[0]))


# ---------------------------------------------------------------------------
# Paired users (DM pairing flow)
# ---------------------------------------------------------------------------


import secrets


@router.get(
    "/api/discord/configs/{config_id}/paired-users",
    response_model=list[PairedUserResponse],
)
async def list_paired_users(config_id: int, db=Depends(get_db)):
    """List all approved paired users for a Discord config."""
    cfg_rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not cfg_rows:
        raise HTTPException(404, detail="Discord config not found")
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_paired_users WHERE discord_config_id = ? ORDER BY paired_at DESC",
        (config_id,),
    )
    return [dict(row_to_dict(r)) for r in rows]


@router.post(
    "/api/discord/configs/{config_id}/paired-users",
    response_model=PairedUserResponse,
    status_code=201,
)
async def create_paired_user(
    config_id: int, body: PairedUserCreate, db=Depends(get_db)
):
    """Approve a Discord user for DM access under 'pairing' dm_policy.

    If pairing_code is empty, a random code is generated.
    If the user is already paired, their record is updated (upsert).
    """
    cfg_rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not cfg_rows:
        raise HTTPException(404, detail="Discord config not found")

    pairing_code = body.pairing_code or secrets.token_hex(4).upper()

    cursor = await db.execute(
        """INSERT INTO discord_paired_users
           (discord_config_id, user_id, username, pairing_code, approved_by)
           VALUES (?, ?, ?, ?, ?)
           ON CONFLICT(discord_config_id, user_id) DO UPDATE SET
               username = excluded.username,
               pairing_code = excluded.pairing_code,
               approved_by = excluded.approved_by,
               paired_at = CURRENT_TIMESTAMP""",
        (
            config_id,
            body.user_id,
            body.username,
            pairing_code,
            body.approved_by,
        ),
    )
    await db.commit()

    rows = await db.execute_fetchall(
        "SELECT * FROM discord_paired_users WHERE discord_config_id = ? AND user_id = ?",
        (config_id, body.user_id),
    )
    return dict(row_to_dict(rows[0]))


@router.delete(
    "/api/discord/configs/{config_id}/paired-users/{user_id}",
    status_code=204,
)
async def delete_paired_user(config_id: int, user_id: str, db=Depends(get_db)):
    """Remove a paired user, revoking their DM access."""
    rows = await db.execute_fetchall(
        "SELECT id FROM discord_paired_users WHERE discord_config_id = ? AND user_id = ?",
        (config_id, user_id),
    )
    if not rows:
        raise HTTPException(404, detail="Paired user not found")
    await db.execute(
        "DELETE FROM discord_paired_users WHERE discord_config_id = ? AND user_id = ?",
        (config_id, user_id),
    )
    await db.commit()


# ---------------------------------------------------------------------------
# Activity logs
# ---------------------------------------------------------------------------


@router.post(
    "/api/discord/activity",
    response_model=ActivityLogResponse,
    status_code=201,
)
async def create_activity_log(body: ActivityLogCreate, db=Depends(get_db)):
    """Record a Discord activity event.

    Called internally by the bot gateway to log lifecycle and messaging events.
    Event types: bot_connected, bot_disconnected, message_received,
    message_denied, message_sent, ai_response, command_invoked.
    """
    cfg_rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (body.config_id,)
    )
    if not cfg_rows:
        raise HTTPException(404, detail="Discord config not found")

    cursor = await db.execute(
        """INSERT INTO discord_activity_logs
           (config_id, event_type, channel_id, user_id, detail)
           VALUES (?, ?, ?, ?, ?)
           RETURNING id""",
        (
            body.config_id,
            body.event_type,
            body.channel_id,
            body.user_id,
            body.detail,
        ),
    )
    new_id = cursor.lastrowid
    await db.commit()
    rows = await db.execute_fetchall(
        "SELECT * FROM discord_activity_logs WHERE id = ?", (new_id,)
    )
    return row_to_dict(rows[0])


@router.get(
    "/api/discord/configs/{config_id}/activity",
    response_model=PaginatedResponse[ActivityLogResponse],
)
async def list_activity_logs(
    config_id: int,
    event_type: str | None = Query(default=None),
    limit: int = Query(default=50, ge=1, le=200),
    offset: int = Query(default=0, ge=0),
    db=Depends(get_db),
):
    """List activity logs for a Discord bot configuration.

    Supports optional filtering by event_type and pagination.
    Returns logs ordered most-recent first.
    """
    cfg_rows = await db.execute_fetchall(
        "SELECT id FROM discord_configs WHERE id = ?", (config_id,)
    )
    if not cfg_rows:
        raise HTTPException(404, detail="Discord config not found")

    if event_type:
        count_rows = await db.execute_fetchall(
            "SELECT COUNT(*) FROM discord_activity_logs WHERE config_id = ? AND event_type = ?",
            (config_id, event_type),
        )
        rows = await db.execute_fetchall(
            "SELECT * FROM discord_activity_logs WHERE config_id = ? AND event_type = ?"
            " ORDER BY created_at DESC LIMIT ? OFFSET ?",
            (config_id, event_type, limit, offset),
        )
    else:
        count_rows = await db.execute_fetchall(
            "SELECT COUNT(*) FROM discord_activity_logs WHERE config_id = ?",
            (config_id,),
        )
        rows = await db.execute_fetchall(
            "SELECT * FROM discord_activity_logs WHERE config_id = ?"
            " ORDER BY created_at DESC LIMIT ? OFFSET ?",
            (config_id, limit, offset),
        )

    total = count_rows[0]["count"] if count_rows else 0
    items = [row_to_dict(r) for r in rows]
    return {
        "items": items,
        "total": total,
        "limit": limit,
        "offset": offset,
        "has_next": (offset + len(items)) < total,
    }
