"""
MIT License Copyright (c) 2025-present June

Permission is hereby granted, free of
charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:

The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""

import asyncio
import logging
import time
from dataclasses import dataclass
from typing import Dict, Optional

logger = logging.getLogger(__name__)


@dataclass
class _UserTimers:
    """Tracks active timers for a user session.

    Attributes:
        inactivity_task: Task waiting for inactivity period.
        close_task: Task waiting for auto-close after confirmation.
    """

    inactivity_task: Optional[asyncio.Task] = None
    close_task: Optional[asyncio.Task] = None


class InactivityManager:
    """Manages inactivity timeouts for WhatsApp sessions.

    Monitors user activity and handles timeouts:
    - On every user message, call reset(from_id)
    - After inactivity_minutes passes, sends "are you still there?" prompt
    - After confirmation_minutes more, auto-closes the session and deletes data

    Attributes:
        prompt_message: Message sent to check if user is still active.
        close_message: Message sent when session auto-closes.
        inactivity_minutes: Minutes before sending activity check prompt.
        confirmation_minutes: Minutes to wait for response before auto-closing.
    """

    def __init__(
        self,
        prompt_message: str = "Are you still there? Please respond to continue the session.",
        close_message: str = "Session closed due to inactivity.",
        inactivity_minutes: int = 15,
        confirmation_minutes: int = 5,
    ):
        """
        Initialize the inactivity manager.

        Args:
            prompt_message: Message to send when inactivity is detected.
            close_message: Message to send when session is auto-closed.
            inactivity_minutes: Minutes of inactivity before sending prompt.
            confirmation_minutes: Minutes to wait for confirmation before closing.
        """
        self.prompt_message = prompt_message
        self.close_message = close_message
        self._inactivity_seconds = inactivity_minutes * 60
        self._confirmation_seconds = confirmation_minutes * 60
        self._timers: Dict[str, _UserTimers] = {}
        self._lock = asyncio.Lock()
        self._adapter = None
        self._redis = None

    def set_adapter(self, adapter):
        """
        Set the adapter to send messages through (e.g., AzureBindingClient).

        Args:
            adapter: The binding client adapter for sending messages.
        """
        self._adapter = adapter
        try:
            self._redis = getattr(adapter.session_store, "client", None)
        except AttributeError:
            self._redis = None

    async def reset(self, from_id: str) -> None:
        """Reset timers for a user. Call on every incoming message.

        Cancels any existing inactivity timers and starts a new one.

        Args:
            from_id: The user ID (phone number) to reset timers for.
        """
        if self._redis:
            ttl = self._inactivity_seconds + self._confirmation_seconds + 60
            await self._redis.set(f"inactivity_last_activity:{from_id}", str(time.time()), ex=int(ttl))

        async with self._lock:
            timers = self._timers.get(from_id) or _UserTimers()

            # cancel existing timers
            if timers.inactivity_task and not timers.inactivity_task.done():
                timers.inactivity_task.cancel()
            if timers.close_task and not timers.close_task.done():
                timers.close_task.cancel()
                timers.close_task = None

            # start new inactivity timer
            timers.inactivity_task = asyncio.create_task(self._schedule_inactivity_prompt(from_id))
            self._timers[from_id] = timers

    async def cancel(self, from_id: str) -> None:
        """Cancel all timers for a user (e.g., when session ends).

        Args:
            from_id: The user ID to cancel timers for.
        """
        async with self._lock:
            timers = self._timers.pop(from_id, None)
            if timers:
                if timers.inactivity_task and not timers.inactivity_task.done():
                    timers.inactivity_task.cancel()
                if timers.close_task and not timers.close_task.done():
                    timers.close_task.cancel()

    async def _schedule_inactivity_prompt(self, from_id: str) -> None:
        """Wait for inactivity period, then send prompt.

        Args:
            from_id: The user ID to send prompt to.
        """
        try:
            await asyncio.sleep(self._inactivity_seconds)
        except asyncio.CancelledError:
            return

        # Send "are you still there?" message
        await self._send_presence_prompt(from_id)

        # schedule auto-close
        async with self._lock:
            timers = self._timers.get(from_id)
            if timers:
                timers.close_task = asyncio.create_task(self._schedule_auto_close(from_id))

    async def _schedule_auto_close(self, from_id: str) -> None:
        """Wait for confirmation period, then close session.

        Args:
            from_id: The user ID whose session to auto-close.
        """
        try:
            await asyncio.sleep(self._confirmation_seconds)
        except asyncio.CancelledError:
            return

        await self._auto_close(from_id)

    async def _send_presence_prompt(self, from_id: str) -> None:
        """Send presence message.

        Args:
            from_id: The user ID (phone number) to send the prompt to.
        """
        lock_key = None
        try:
            # skip if user is awaiting an async response
            if await self._adapter.session_store.get_awaiting_flag(from_id):
                return

            if self._redis:
                # skip if user was recently active on another instance
                last_activity_raw = await self._redis.get(f"inactivity_last_activity:{from_id}")
                if last_activity_raw:
                    time_since = time.time() - float(last_activity_raw)
                    if time_since < self._inactivity_seconds:
                        return

                # acquire distributed prompt lock (SET NX)
                lock_key = f"inactivity_prompt_lock:{from_id}"
                acquired = await self._redis.set(lock_key, "1", nx=True, ex=90)
                if not acquired:
                    return

                # skip if prompt was already sent (dedup across instances)
                sent_key = f"inactivity_prompt_sent:{from_id}"
                already_sent = await self._redis.get(sent_key)
                if already_sent:
                    return
                await self._redis.set(sent_key, "1", ex=int(self._confirmation_seconds))

            if not self._adapter.client:
                return

            channel_id = getattr(self._adapter, "channel_id", None)
            if channel_id:
                from azure.communication.messages.models import TextNotificationContent

                content = TextNotificationContent(
                    channel_registration_id=channel_id,
                    to=[from_id],
                    content=self.prompt_message,
                )
                await self._adapter.client.send(content)
            logger.info(f"Sent inactivity prompt to {from_id}")
        except Exception as e:
            logger.error(f"Failed to send inactivity prompt: {e}")
        finally:
            if self._redis and lock_key:
                await self._redis.delete(lock_key)

    async def _auto_close(self, from_id: str) -> None:
        """Auto-close the session due to inactivity.

        Args:
            from_id: The user ID (phone number) whose session to close.
        """
        lock_key = None
        try:
            # skip if user is awaiting an async response
            if await self._adapter.session_store.get_awaiting_flag(from_id):
                return

            if self._redis:
                # skip if user was recently active (activity within inactivity + confirmation window)
                last_activity_raw = await self._redis.get(f"inactivity_last_activity:{from_id}")
                if last_activity_raw:
                    time_since = time.time() - float(last_activity_raw)
                    if time_since < (self._inactivity_seconds + self._confirmation_seconds):
                        return

                # acquire distributed close lock (SET NX)
                lock_key = f"inactivity_close_lock:{from_id}"
                acquired = await self._redis.set(lock_key, "1", nx=True, ex=90)
                if not acquired:
                    return

                # skip if already closed on another instance
                closed_key = f"inactivity_closed:{from_id}"
                if await self._redis.get(closed_key):
                    return
                await self._redis.set(closed_key, "1", ex=60)

            if not self._adapter.client:
                return

            channel_id = getattr(self._adapter, "channel_id", None)
            if channel_id:
                from azure.communication.messages.models import TextNotificationContent

                content = TextNotificationContent(
                    channel_registration_id=channel_id,
                    to=[from_id],
                    content=self.close_message,
                )
                await self._adapter.client.send(content)
            logger.info(f"Auto-closed session for {from_id}")

            # clear session from storage
            await self._adapter.session_store.delete(from_id)

            # clean up timers
            async with self._lock:
                self._timers.pop(from_id, None)
        except Exception as e:
            logger.error(f"Failed to auto-close session: {e}")
        finally:
            if self._redis and lock_key:
                await self._redis.delete(lock_key)
