from __future__ import annotations

"""
Copyright (C) 2025 Luciano Guerche

This file is part of rabbitmq-mcp-server.

rabbitmq-mcp-server is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

rabbitmq-mcp-server is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with rabbitmq-mcp-server. If not, see <https://www.gnu.org/licenses/>.
"""

"""Reconnection manager with exponential backoff for automatic connection recovery."""

import asyncio
from datetime import UTC, datetime
from typing import TYPE_CHECKING

from rabbitmq_mcp_connection.logging.config import get_logger
from rabbitmq_mcp_connection.schemas.connection import ConnectionState
from rabbitmq_mcp_connection.schemas.reconnect import ReconnectionState

if TYPE_CHECKING:
    from rabbitmq_mcp_connection.connection.manager import ConnectionManager
    from rabbitmq_mcp_connection.http_client import HTTPClientManager

logger = get_logger(__name__)


class ReconnectionManager:
    """Manages automatic reconnection with exponential backoff.

    Implements AC #1-9:
    - Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, then 60s max
    - Indefinite reconnection attempts
    - Logs each attempt
    - Reconnects both AMQP and HTTP
    - Tracks metrics
    - Exposes reconnection state
    - Resets backoff after 5 minutes stable connection
    """

    # Exponential backoff schedule (AC #1)
    BACKOFF_SCHEDULE = [1, 2, 4, 8, 16, 32]
    MAX_BACKOFF = 60  # seconds

    def __init__(
        self,
        amqp_manager: ConnectionManager,
        http_manager: HTTPClientManager,
        max_delay: int = 60,
        reset_after: int = 300,
    ) -> None:
        """Initialize reconnection manager.

        Args:
            amqp_manager: AMQP connection manager
            http_manager: HTTP client manager
            max_delay: Maximum reconnection delay in seconds (default: 60)
            reset_after: Time in seconds after which backoff resets (default: 300)
        """
        self._amqp_manager = amqp_manager
        self._http_manager = http_manager
        self._max_delay = max_delay
        self._reset_after = reset_after

        self._state = ConnectionState.DISCONNECTED
        self._attempt = 0
        self._next_attempt_in = 0
        self._total_downtime = 0.0
        self._last_reconnect_timestamp: datetime | None = None
        self._connection_start: datetime | None = None
        self._downtime_start: datetime | None = None

        self._reconnecting = False
        self._reconnect_lock = asyncio.Lock()
        self._reconnect_task: asyncio.Task[None] | None = None
        self._reset_task: asyncio.Task[None] | None = None

    def _calculate_backoff_delay(self, attempt: int) -> int:
        """Calculate exponential backoff delay for given attempt.

        Args:
            attempt: Current attempt number (1-indexed)

        Returns:
            Delay in seconds following exponential backoff schedule
        """
        if attempt <= 0:
            return 1

        # Use schedule for first 6 attempts, then max delay
        if attempt <= len(self.BACKOFF_SCHEDULE):
            return self.BACKOFF_SCHEDULE[attempt - 1]

        return min(self._max_delay, self.MAX_BACKOFF)

    async def handle_connection_loss(self) -> None:
        """Handle connection loss with exponential backoff and indefinite retry (AC #1, #2, #3).

        This method implements the main reconnection loop:
        - Uses exponential backoff schedule
        - Retries indefinitely until successful
        - Logs each attempt
        - Attempts both AMQP and HTTP reconnection
        - Only succeeds when both connections established
        """
        async with self._reconnect_lock:
            if self._reconnecting:
                logger.debug(
                    "Reconnection already in progress, skipping duplicate trigger"
                )
                return

            self._reconnecting = True
            self._state = ConnectionState.RECONNECTING
            self._attempt = 0
            self._downtime_start = datetime.now(UTC)

        logger.info("Connection lost, starting reconnection process")

        try:
            while True:
                self._attempt += 1
                delay = self._calculate_backoff_delay(self._attempt)
                self._next_attempt_in = delay

                logger.info(
                    f"Reconnection attempt {self._attempt} in {delay}s...",
                    attempt=self._attempt,
                    delay=delay,
                )

                await asyncio.sleep(delay)

                # Track downtime
                if self._downtime_start:
                    self._total_downtime = (
                        datetime.now(UTC) - self._downtime_start
                    ).total_seconds()

                # Attempt reconnection
                amqp_connected = await self._try_amqp_reconnection()
                http_connected = await self._try_http_reconnection()

                # Check if both connections successful (AC #5)
                if amqp_connected and http_connected:
                    logger.info("Reconnected to RabbitMQ successfully")
                    self._state = ConnectionState.CONNECTED
                    self._last_reconnect_timestamp = datetime.now(UTC)
                    self._connection_start = datetime.now(UTC)
                    self._reconnecting = False

                    # Schedule backoff reset after stable connection (AC #9)
                    if self._reset_task:
                        self._reset_task.cancel()
                    self._reset_task = asyncio.create_task(
                        self._schedule_backoff_reset()
                    )

                    return

                # Partial failure logging (AC #5)
                if amqp_connected and not http_connected:
                    logger.warning(
                        "Partial reconnection: AMQP connected, HTTP failed - retrying...",
                    )
                elif http_connected and not amqp_connected:
                    logger.warning(
                        "Partial reconnection: HTTP connected, AMQP failed - retrying...",
                    )
                else:
                    logger.debug(
                        "Both AMQP and HTTP reconnection failed, will retry",
                        attempt=self._attempt,
                    )

        except asyncio.CancelledError:
            logger.info("Reconnection cancelled during shutdown")
            self._reconnecting = False
            raise
        except Exception as e:
            logger.error("Unexpected error during reconnection", error=str(e))
            self._reconnecting = False
            raise

    async def _try_amqp_reconnection(self) -> bool:
        """Attempt to reconnect AMQP connection.

        Returns:
            True if connection successful, False otherwise
        """
        try:
            await self._amqp_manager.connect()
            logger.debug("AMQP reconnection successful")
            return True
        except Exception as e:
            logger.debug("AMQP reconnection failed", error=str(e))
            return False

    async def _try_http_reconnection(self) -> bool:
        """Attempt to verify HTTP connection.

        Returns:
            True if HTTP connection working, False otherwise
        """
        try:
            # Try a simple health check request
            response = await self._http_manager.request("GET", "/overview")
            success = response.status_code == 200
            if success:
                logger.debug("HTTP reconnection successful")
            else:
                logger.debug(
                    "HTTP reconnection failed",
                    status_code=response.status_code,
                )
            return bool(success)
        except Exception as e:
            logger.debug("HTTP reconnection failed", error=str(e))
            return False

    async def _schedule_backoff_reset(self) -> None:
        """Schedule backoff reset after stable connection (AC #9).

        Resets backoff to 1s after connection stable for configured duration.
        """
        try:
            await asyncio.sleep(self._reset_after)

            # Reset backoff if connection still stable
            if self._state == ConnectionState.CONNECTED:
                logger.debug(
                    "Connection stable for 5 minutes, backoff reset",
                    reset_after=self._reset_after,
                )
                self._attempt = 0

        except asyncio.CancelledError:
            logger.debug("Backoff reset task cancelled")
            raise

    def get_state(self) -> ReconnectionState:
        """Get current reconnection state (AC #8).

        Returns:
            ReconnectionState with current state, attempt, and metrics
        """
        return ReconnectionState(
            state=self._state.value,
            attempt=self._attempt,
            next_attempt_in_seconds=(
                self._next_attempt_in if self._reconnecting else None
            ),
            total_downtime_seconds=self._total_downtime,
            last_reconnect_timestamp=self._last_reconnect_timestamp,
        )

    async def shutdown(self) -> None:
        """Gracefully shutdown reconnection manager."""
        if self._reconnect_task and not self._reconnect_task.done():
            self._reconnect_task.cancel()
            try:
                await self._reconnect_task
            except asyncio.CancelledError:
                pass

        if self._reset_task and not self._reset_task.done():
            self._reset_task.cancel()
            try:
                await self._reset_task
            except asyncio.CancelledError:
                pass

        self._reconnecting = False
        logger.debug("Reconnection manager shutdown complete")
