"""Slack handler for Lambda-based AI agents.

This module provides a complete Slack handler that manages all the boilerplate:
- URL verification challenges
- Signature verification
- Event parsing
- Error handling
- Response formatting
- Event deduplication (DynamoDB-based for Lambda horizontal scaling)
- Bot message filtering (prevents recursive loops)

Usage:
    from agent_factory.handlers import SlackAgentHandler
    from agent_factory.channels import SlackChannel
    from agent_factory.core import AgentOrchestrator

    handler = SlackAgentHandler(
        orchestrator=my_orchestrator,
        channel=SlackChannel(
            bot_token="xoxb-...",
            signing_secret="...",
        ),
        dedup_table_name="my-dedup-table",  # Optional: DynamoDB table for deduplication
    )

    # In Lambda
    def lambda_handler(event, context):
        return handler.handle(event, context)
"""

import json
import os
import time
from typing import Any, Callable, Dict, Optional, Set

from agent_factory.channels.base import ChannelAccessDeniedError
from agent_factory.channels.slack import SlackChannel
from agent_factory.core.orchestrator import AgentOrchestrator
from agent_factory.observability.logging import get_logger, log_handler_complete, log_handler_start
from agent_factory.observability.metrics import get_metrics, record_invocation, record_latency

logger = get_logger("slack_handler")

# In-memory cache as fallback (works for warm Lambda instances)
_processed_events: Set[str] = set()
_MAX_CACHE_SIZE = 1000

# DynamoDB client (lazy initialized)
_dynamodb_client = None


def _get_dynamodb_client():
    """Get or create DynamoDB client."""
    global _dynamodb_client
    if _dynamodb_client is None:
        import boto3

        _dynamodb_client = boto3.client("dynamodb")
    return _dynamodb_client


def _try_claim_event(table_name: str, dedup_key: str, ttl_seconds: int = 300) -> bool:
    """Try to claim an event for processing using DynamoDB conditional write.

    Returns True if this is the first Lambda to claim the event, False if already claimed.
    Uses conditional write to ensure atomicity across concurrent Lambda instances.
    """
    try:
        client = _get_dynamodb_client()
        ttl = int(time.time()) + ttl_seconds

        client.put_item(
            TableName=table_name,
            Item={
                "pk": {"S": dedup_key},
                "ttl": {"N": str(ttl)},
                "claimed_at": {"N": str(int(time.time() * 1000))},
            },
            ConditionExpression="attribute_not_exists(pk)",
        )
        return True  # Successfully claimed
    except Exception as e:
        if "ConditionalCheckFailedException" in str(type(e).__name__):
            return False  # Already claimed by another Lambda
        # Log but don't fail on other errors - fall back to in-memory
        logger.warning(f"DynamoDB dedup error: {e}")
        return True  # Allow processing on error (fail open)


class SlackAgentHandler:
    """Complete Slack handler for AI agents.

    Handles all Slack integration boilerplate so agents only need to
    define their tools and system prompt.

    Example:
        from agent_factory.handlers import SlackAgentHandler
        from agent_factory.channels import SlackChannel
        from agent_factory.models import BedrockModel
        from agent_factory.core import AgentOrchestrator

        # Create orchestrator with your tools
        orchestrator = AgentOrchestrator(
            model=BedrockModel(),
            system_prompt="You are Berta, a finance assistant...",
            tools=[QueryInvoicesTool(), SearchDocsTool()],
        )

        # Create handler
        handler = SlackAgentHandler(
            orchestrator=orchestrator,
            channel=SlackChannel(),
        )

        # Lambda entry point
        def lambda_handler(event, context):
            return handler.handle(event, context)
    """

    def __init__(
        self,
        orchestrator: AgentOrchestrator,
        channel: Optional[SlackChannel] = None,
        *,
        on_error: Optional[Callable[[Exception], str]] = None,
        thinking_emoji: str = "thinking_face",
        error_emoji: str = "x",
        dedup_table_name: Optional[str] = None,
    ):
        """Initialize the Slack handler.

        Args:
            orchestrator: Agent orchestrator to process messages
            channel: Slack channel (creates default if not provided)
            on_error: Custom error handler that returns user-facing message
            thinking_emoji: Emoji to add while processing
            error_emoji: Emoji to add on error
            dedup_table_name: DynamoDB table for event deduplication (recommended for Lambda).
                              Can also be set via SLACK_DEDUP_TABLE env var.
                              Table must have 'pk' as partition key and optional 'ttl' for TTL.
        """
        self.orchestrator = orchestrator
        self.channel = channel or SlackChannel()
        self.on_error = on_error
        self.thinking_emoji = thinking_emoji
        self.error_emoji = error_emoji
        self.dedup_table_name = dedup_table_name or os.environ.get("SLACK_DEDUP_TABLE")
        self.metrics = get_metrics()

        # Add channel to orchestrator if not already added
        if self.channel.channel_type.value not in orchestrator._channels:
            orchestrator.add_channel(self.channel)

    def handle(self, event: Dict[str, Any], context: Any = None) -> Dict[str, Any]:
        """Handle a Lambda event from API Gateway or direct invocation.

        This method handles:
        - URL verification challenges
        - Signature verification
        - Event parsing and routing
        - Error handling and response formatting

        Args:
            event: Lambda event (API Gateway format or direct)
            context: Lambda context (optional)

        Returns:
            Lambda response dict with statusCode and body
        """
        start_time = time.time()
        log_handler_start(logger, "slack_handler", event)

        try:
            # Parse body from API Gateway
            body = self._parse_body(event)
            headers = self._get_headers(event)

            # Verify signature (skip for URL verification)
            if body.get("type") != "url_verification":
                body_bytes = self._get_raw_body(event)
                if not self.channel.verify_request(headers, body_bytes):
                    logger.warning("Invalid Slack signature")
                    return self._response(401, {"error": "Invalid signature"})

            # Handle URL verification
            if self.channel.is_url_verification(body):
                logger.info("Handling URL verification challenge")
                return self.channel.get_url_verification_response(body)

            # Process the event
            result = self._process_event(body)

            duration_ms = (time.time() - start_time) * 1000
            log_handler_complete(logger, "slack_handler", status_code=200, duration_ms=duration_ms)
            record_invocation(self.metrics, "slack_handler", success=True)
            record_latency(self.metrics, "slack_handler", duration_ms)

            return result

        except ChannelAccessDeniedError as e:
            logger.warning(f"Access denied: {e}")
            return self._response(200, {"ok": True})  # Silent ignore for unauthorized channels

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.exception("Handler error")
            record_invocation(self.metrics, "slack_handler", success=False)
            record_latency(self.metrics, "slack_handler", duration_ms)

            return self._response(500, {"error": str(e)})

    def _parse_body(self, event: Dict[str, Any]) -> Dict[str, Any]:
        """Parse body from various event formats."""
        body = event.get("body", event)

        if isinstance(body, str):
            try:
                return json.loads(body)
            except json.JSONDecodeError:
                return {}

        return body if isinstance(body, dict) else {}

    def _get_headers(self, event: Dict[str, Any]) -> Dict[str, str]:
        """Get headers with case-insensitive access."""
        headers = event.get("headers", {}) or {}
        # Normalize to standard Slack header format
        normalized = {}
        for key, value in headers.items():
            normalized[key] = value
            # Add common variations
            if key.lower() == "x-slack-request-timestamp":
                normalized["X-Slack-Request-Timestamp"] = value
            elif key.lower() == "x-slack-signature":
                normalized["X-Slack-Signature"] = value
        return normalized

    def _get_raw_body(self, event: Dict[str, Any]) -> bytes:
        """Get raw body bytes for signature verification."""
        body = event.get("body", "")
        if isinstance(body, bytes):
            return body
        return body.encode("utf-8") if body else b""

    def _process_event(self, body: Dict[str, Any]) -> Dict[str, Any]:
        """Process a Slack event."""
        event_type = body.get("type")

        if event_type == "event_callback":
            return self._handle_event_callback(body)

        # Unknown event type - acknowledge
        logger.info(f"Unhandled event type: {event_type}")
        return self._response(200, {"ok": True})

    def _handle_event_callback(self, body: Dict[str, Any]) -> Dict[str, Any]:
        """Handle event_callback events (messages, mentions, etc.)."""
        global _processed_events

        inner_event = body.get("event", {})
        event_type = inner_event.get("type")

        # Message deduplication using channel+ts+event_type (unique per event)
        # Slack sends both 'message' and 'app_mention' events for the same mention,
        # so we include event_type to avoid the message event claiming the dedup key
        # and preventing the app_mention event from being processed.
        channel_id = inner_event.get("channel", "")
        message_ts = inner_event.get("ts", "")
        dedup_key = f"{channel_id}:{message_ts}:{event_type}" if channel_id and message_ts else None

        if dedup_key:
            # Try DynamoDB-based deduplication first (works across Lambda instances)
            if self.dedup_table_name:
                if not _try_claim_event(self.dedup_table_name, dedup_key):
                    logger.info(f"Skipping duplicate message (DynamoDB): {dedup_key}")
                    return self._response(200, {"ok": True})
            else:
                # Fall back to in-memory deduplication (only works within same Lambda instance)
                if dedup_key in _processed_events:
                    logger.info(f"Skipping duplicate message (memory): {dedup_key}")
                    return self._response(200, {"ok": True})

                # Add to cache with size limit
                _processed_events.add(dedup_key)
                if len(_processed_events) > _MAX_CACHE_SIZE:
                    # Remove oldest entries
                    _processed_events.clear()

        # Skip bot messages to prevent loops - check multiple indicators
        if self._is_bot_message(inner_event, body):
            logger.info("Skipping bot message to prevent loop")
            return self._response(200, {"ok": True})

        # Handle app_mention events (always respond when mentioned)
        if event_type == "app_mention":
            return self._handle_message(body)

        # Handle message events (only respond in DMs or threads)
        if event_type == "message":
            # Check if this is a DM (channel type starts with D)
            channel_type = inner_event.get("channel_type", "")
            is_dm = channel_type == "im" or channel_id.startswith("D")

            # Check if this is in a thread
            is_thread = inner_event.get("thread_ts") is not None

            if is_dm or is_thread:
                return self._handle_message(body)
            else:
                # Ignore messages in channels without mention
                logger.info("Ignoring message in channel without mention")
                return self._response(200, {"ok": True})

        logger.info(f"Unhandled inner event type: {event_type}")
        return self._response(200, {"ok": True})

    def _is_bot_message(self, event: Dict[str, Any], body: Dict[str, Any]) -> bool:
        """Check if the message is from a bot to prevent recursive loops.

        Checks multiple indicators:
        - bot_id field (most common)
        - subtype == "bot_message"
        - app_id matching our app (message from this bot)
        - user starts with 'B' (bot user IDs start with B, or actually sometimes they don't)
        """
        # Direct bot indicators
        if event.get("bot_id"):
            return True
        if event.get("subtype") == "bot_message":
            return True

        # Check if message is from our own app
        # The authorizations field contains info about the installing app
        authorizations = body.get("authorizations", [])
        if authorizations:
            our_user_id = authorizations[0].get("user_id")
            if our_user_id and event.get("user") == our_user_id:
                return True

        # Check for bot_profile (some bot messages include this)
        if event.get("bot_profile"):
            return True

        return False

    def _handle_message(self, body: Dict[str, Any]) -> Dict[str, Any]:
        """Handle a message event."""
        inner_event = body.get("event", {})

        channel_id = inner_event.get("channel", "")
        user_id = inner_event.get("user", "")
        text = inner_event.get("text", "")
        thread_ts = inner_event.get("thread_ts") or inner_event.get("ts")
        ts = inner_event.get("ts", "")

        if not text or not channel_id:
            return self._response(200, {"ok": True})

        logger.info(
            "Processing Slack message",
            extra={
                "channel_id": channel_id,
                "user_id": user_id,
                "thread_ts": thread_ts,
            },
        )

        # Add thinking reaction
        if ts:
            self.channel.add_reaction(channel_id, ts, self.thinking_emoji)

        try:
            # Build session ID from thread
            session_id = f"slack:{channel_id}:{thread_ts}" if thread_ts else f"slack:{channel_id}"

            # Build context
            context = {
                "channel_type": "slack",
                "channel_id": channel_id,
                "user_id": user_id,
                "slack_user_id": user_id,
                "team_id": body.get("team_id"),
                "thread_ts": thread_ts,
            }

            # Process message
            result = self.orchestrator.invoke(
                prompt=text,
                session_id=session_id,
                context=context,
            )

            # Send response
            response_text = result.get("result", "I'm sorry, I couldn't process that.")
            self.channel.send_message(
                self.channel.format_response(
                    text=response_text,
                    channel_id=channel_id,
                    thread_id=thread_ts,
                )
            )

            return self._response(200, {"ok": True})

        except Exception as e:
            logger.exception("Error processing message")

            # Add error reaction
            if ts:
                self.channel.add_reaction(channel_id, ts, self.error_emoji)

            # Send error message
            error_message = "I encountered an error processing your request."
            if self.on_error:
                error_message = self.on_error(e)

            self.channel.send_message(
                self.channel.format_response(
                    text=error_message,
                    channel_id=channel_id,
                    thread_id=thread_ts,
                )
            )

            return self._response(200, {"ok": True})

    def _response(
        self,
        status_code: int,
        body: Dict[str, Any],
        headers: Optional[Dict[str, str]] = None,
    ) -> Dict[str, Any]:
        """Create a Lambda response."""
        default_headers = {
            "Content-Type": "application/json",
        }
        if headers:
            default_headers.update(headers)

        return {
            "statusCode": status_code,
            "headers": default_headers,
            "body": json.dumps(body),
        }


def create_slack_handler(
    orchestrator: AgentOrchestrator,
    bot_token: Optional[str] = None,
    signing_secret: Optional[str] = None,
    allowed_channels: Optional[list[str]] = None,
) -> Callable[[Dict[str, Any], Any], Dict[str, Any]]:
    """Factory function to create a Slack Lambda handler.

    This is a convenience function that creates the handler and returns
    the handle method directly.

    Args:
        orchestrator: Agent orchestrator
        bot_token: Slack bot token (or use env var)
        signing_secret: Slack signing secret (or use env var)
        allowed_channels: Optional list of allowed channel IDs

    Returns:
        Lambda handler function

    Example:
        from agent_factory.handlers import create_slack_handler

        handler = create_slack_handler(my_orchestrator)

        # This can be used directly as the Lambda handler
        # handler(event, context)
    """
    channel = SlackChannel(
        bot_token=bot_token,
        signing_secret=signing_secret,
        allowed_channels=allowed_channels,
    )

    slack_handler = SlackAgentHandler(
        orchestrator=orchestrator,
        channel=channel,
    )

    return slack_handler.handle
