"""
Slack channel adapter.

Handles Slack Events API events via slack-bolt AsyncApp.

EVENT FLOW:
  1. Slack sends event to /slack/events (HTTP 200 must be returned in <3s)
  2. Handler normalizes event -> KonstructMessage
  3. Tenant resolved from workspace_id
  4. Rate limit checked
  5. Idempotency checked (prevents duplicate processing on Slack retries)
  6. Placeholder "Thinking..." message posted in-thread (typing indicator)
  7. Celery task dispatched with message + placeholder details
  8. Celery worker calls LLM pool, replaces placeholder with real response

CRITICAL: DO NOT perform any LLM work inside event handlers.
Slack retries after 3 seconds if HTTP 200 is not received. All heavyweight
work must be dispatched to Celery before returning.

THREAD FOLLOW-UP:
  After the first @mention in a thread, subsequent messages in that thread
  (even without @mention) trigger a response for 30 minutes of idle time.
  This is tracked via Redis engaged_thread_key with a 30-minute TTL.
"""

from __future__ import annotations

import logging

from redis.asyncio import Redis
from slack_bolt.async_app import AsyncApp
from sqlalchemy.ext.asyncio import AsyncSession

from gateway.normalize import normalize_slack_event
from router.idempotency import is_duplicate
from router.ratelimit import RateLimitExceeded, check_rate_limit
from router.tenant import resolve_tenant
from shared.redis_keys import engaged_thread_key

logger = logging.getLogger(__name__)

# How long a thread stays "engaged" after the last @mention (seconds)
_ENGAGED_THREAD_TTL = 1800  # 30 minutes


def register_slack_handlers(
    slack_app: AsyncApp,
    redis: Redis,  # type: ignore[type-arg]
    get_session: object,  # Callable returning AsyncSession context manager
) -> None:
    """
    Register Slack event handlers on the slack-bolt AsyncApp.

    Call this once at application startup after creating the AsyncApp instance.

    Args:
        slack_app:    The slack-bolt AsyncApp instance.
        redis:        Async Redis client for rate limiting + idempotency.
        get_session:  Async context manager factory for DB sessions.
                      Typically ``shared.db.async_session_factory``.
    """

    @slack_app.event("app_mention")
    async def handle_app_mention(event: dict, say: object, client: object) -> None:
        """
        Handle @mention events in channels.

        Called when a user @mentions the bot in any channel the bot belongs to.
        """
        await _handle_slack_event(
            event=event,
            say=say,
            client=client,
            redis=redis,
            get_session=get_session,
            event_type="app_mention",
        )

    @slack_app.event("message")
    async def handle_message(event: dict, say: object, client: object) -> None:
        """
        Handle direct messages (DMs).

        Filtered to channel_type=="im" only — ignores channel messages
        to avoid double-processing @mentions.
        """
        # Only handle DMs to prevent double-triggering alongside app_mention
        if event.get("channel_type") != "im":
            return

        # Ignore bot messages to prevent infinite response loops
        if event.get("bot_id") or event.get("subtype") == "bot_message":
            return

        await _handle_slack_event(
            event=event,
            say=say,
            client=client,
            redis=redis,
            get_session=get_session,
            event_type="dm",
        )


async def _handle_slack_event(
    event: dict,
    say: object,
    client: object,
    redis: Redis,  # type: ignore[type-arg]
    get_session: object,
    event_type: str,
) -> None:
    """
    Shared handler logic for app_mention and DM message events.

    Performs: normalize -> tenant resolve -> rate limit -> idempotency ->
    post placeholder -> dispatch Celery -> mark thread engaged.

    All work is dispatched to Celery before returning. HTTP 200 is returned
    to Slack immediately by slack-bolt after this coroutine completes.
    """
    # Ignore bot messages (double-check here for safety)
    if event.get("bot_id") or event.get("subtype") == "bot_message":
        return

    # Extract workspace_id from the outer context — injected via middleware
    # or extracted from the Slack signing payload.
    workspace_id: str = event.get("_workspace_id", "")
    bot_user_id: str = event.get("_bot_user_id", "")

    # Step 1: Normalize to KonstructMessage
    msg = normalize_slack_event(
        event=event,
        workspace_id=workspace_id,
        bot_user_id=bot_user_id,
    )

    # Step 2: Resolve tenant from workspace_id
    tenant_id: str | None = None
    async with get_session() as session:  # type: ignore[attr-defined]
        tenant_id = await resolve_tenant(
            workspace_id=workspace_id,
            channel_type="slack",
            session=session,
        )

    if tenant_id is None:
        logger.warning(
            "handle_slack_event: unknown workspace_id=%r event_type=%s — ignoring",
            workspace_id,
            event_type,
        )
        return

    msg.tenant_id = tenant_id

    # Step 3: Check rate limit — post ephemeral rejection if exceeded
    try:
        await check_rate_limit(
            tenant_id=tenant_id,
            channel="slack",
            redis=redis,
        )
    except RateLimitExceeded as exc:
        logger.info(
            "Rate limit exceeded: tenant=%s — posting ephemeral rejection",
            tenant_id,
        )
        # Post ephemeral message visible only to the requesting user
        try:
            await client.chat_postEphemeral(  # type: ignore[union-attr]
                channel=event.get("channel", ""),
                user=event.get("user", ""),
                text=(
                    f"I'm receiving too many requests right now. "
                    f"Please try again in about {exc.remaining_seconds} seconds."
                ),
            )
        except Exception:
            logger.exception("Failed to post rate limit ephemeral message")
        return

    # Step 4: Idempotency check — skip duplicate events (Slack retry protection)
    event_ts: str = event.get("ts", msg.id)
    if await is_duplicate(tenant_id, event_ts, redis):
        logger.debug(
            "Duplicate Slack event: tenant=%s event_ts=%s — skipping",
            tenant_id,
            event_ts,
        )
        return

    # Step 5: Check thread engagement — auto-follow messages in engaged threads
    thread_id = msg.thread_id or event_ts
    is_engaged = await _is_engaged_thread(tenant_id, thread_id, redis)

    # For channel messages: only respond to @mentions or engaged threads
    if event_type != "dm" and not is_engaged:
        # This shouldn't happen for app_mention, but guard defensively
        # (e.g., if the event somehow arrives without mention metadata)
        pass  # Fall through to dispatch — app_mention always warrants a response

    # Step 6: Post placeholder "Thinking..." message in thread
    channel_id: str = event.get("channel", "")
    placeholder_ts: str = ""

    try:
        placeholder_resp = await client.chat_postMessage(  # type: ignore[union-attr]
            channel=channel_id,
            thread_ts=thread_id,
            text="_Thinking..._",
        )
        placeholder_ts = placeholder_resp.get("ts", "")  # type: ignore[union-attr]
    except Exception:
        logger.exception(
            "Failed to post placeholder message: tenant=%s channel=%s thread=%s",
            tenant_id,
            channel_id,
            thread_id,
        )
        # Continue even if placeholder fails — still dispatch to Celery
        # The Celery task will post a new message instead of updating

    # Step 7: Dispatch to Celery (fire-and-forget)
    # Import here to avoid circular imports at module load time
    from orchestrator.tasks import handle_message as handle_message_task  # noqa: PLC0415

    task_payload = msg.model_dump() | {
        "placeholder_ts": placeholder_ts,
        "channel_id": channel_id,
    }

    try:
        handle_message_task.delay(task_payload)
    except Exception:
        logger.exception(
            "Failed to dispatch handle_message task: tenant=%s msg_id=%s",
            tenant_id,
            msg.id,
        )
        return

    # Step 8: Mark thread as engaged (auto-follow for 30 minutes)
    await _mark_thread_engaged(tenant_id, thread_id, redis)

    logger.info(
        "Dispatched: event_type=%s tenant=%s msg_id=%s thread=%s",
        event_type,
        tenant_id,
        msg.id,
        thread_id,
    )


async def _is_engaged_thread(
    tenant_id: str,
    thread_id: str,
    redis: Redis,  # type: ignore[type-arg]
) -> bool:
    """Check if a thread is currently engaged (bot responded recently)."""
    key = engaged_thread_key(tenant_id, thread_id)
    try:
        result = await redis.exists(key)
        return bool(result)
    except Exception:
        logger.exception("Failed to check engaged thread: tenant=%s thread=%s", tenant_id, thread_id)
        return False


async def _mark_thread_engaged(
    tenant_id: str,
    thread_id: str,
    redis: Redis,  # type: ignore[type-arg]
) -> None:
    """Mark a thread as engaged with a 30-minute TTL."""
    key = engaged_thread_key(tenant_id, thread_id)
    try:
        await redis.set(key, "1", ex=_ENGAGED_THREAD_TTL)
    except Exception:
        logger.exception("Failed to mark thread engaged: tenant=%s thread=%s", tenant_id, thread_id)
