"""Async client for Pentair Thermal WiFi API."""

from __future__ import annotations

import asyncio
import logging
from collections.abc import AsyncIterator, Awaitable, Callable
from datetime import datetime, timedelta, timezone
from typing import Any

import httpx

from ._common import calculate_backoff_delay, mask_sensitive_params
from .constants import (
    BASE_URL,
    DEFAULT_MAX_RETRIES,
    DEFAULT_NOTIFICATION_TIMEOUT,
    DEFAULT_TIMEOUT,
    ENDPOINT_AUTHENTICATE,
    ENDPOINT_NOTIFICATION,
    ENDPOINT_THERMOSTAT,
    ENDPOINT_THERMOSTATS,
)
from .exceptions import (
    APIError,
    AuthenticationError,
    SessionExpiredError,
    ThermostatNotFoundError,
)
from .models import (
    AuthResponse,
    Notification,
    RegulationMode,
    Thermostat,
    ThermostatsResponse,
    UpdateThermostatResponse,
)

logger = logging.getLogger(__name__)


class AsyncPentairThermalWifi:
    """Async client for Pentair Thermal WiFi API."""

    def __init__(
        self,
        email: str,
        password: str,
        max_retries: int = DEFAULT_MAX_RETRIES,
        timeout: float = DEFAULT_TIMEOUT,
    ) -> None:
        """Initialize the async client.

        Args:
            email: Account email
            password: Account password
            max_retries: Maximum number of retry attempts for failed requests
            timeout: Request timeout in seconds
        """
        self.email = email
        self.password = password
        self.max_retries = max_retries
        self.timeout = timeout
        self._session_id: str | None = None
        self._client: httpx.AsyncClient | None = None
        self._monitoring_task: asyncio.Task[None] | None = None
        self._monitoring_stop_event: asyncio.Event = asyncio.Event()
        self._thermostats_fetched: bool = False  # Track if thermostats have been fetched
        logger.info(f"Initialized AsyncPentairThermalWifi client for {email}")

    async def __aenter__(self) -> AsyncPentairThermalWifi:
        """Enter async context manager."""
        self._client = httpx.AsyncClient(timeout=self.timeout)
        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Exit async context manager."""
        if self._client:
            await self._client.aclose()
            self._client = None

    def _get_client(self) -> httpx.AsyncClient:
        """Get or create HTTP client."""
        if self._client is None:
            self._client = httpx.AsyncClient(timeout=self.timeout)
        return self._client

    async def _request_with_retry(
        self,
        method: str,
        url: str,
        **kwargs: Any,
    ) -> httpx.Response:
        """Make HTTP request with retry logic.

        Args:
            method: HTTP method (GET, POST, etc.)
            url: Request URL
            **kwargs: Additional arguments for httpx

        Returns:
            HTTP response

        Raises:
            APIError: If request fails after all retries
        """
        client = self._get_client()
        last_exception: httpx.HTTPStatusError | httpx.RequestError | None = None

        # Log request details (excluding sensitive data)
        log_params = mask_sensitive_params(kwargs.get("params", {}))
        logger.debug(f"Making {method} request to {url} with params: {log_params}")

        for attempt in range(self.max_retries):
            try:
                response = await client.request(method, url, **kwargs)
                response.raise_for_status()
                logger.debug(f"Request successful: {method} {url} -> {response.status_code}")
                return response
            except httpx.HTTPStatusError as e:
                last_exception = e
                logger.warning(
                    f"HTTP error {e.response.status_code} on attempt "
                    f"{attempt + 1}/{self.max_retries}"
                )
                if e.response.status_code == 401:
                    logger.error("Session expired (401 Unauthorized)")
                    raise SessionExpiredError("Session expired, please re-authenticate") from e
                if attempt == self.max_retries - 1:
                    break
                wait_time = calculate_backoff_delay(attempt)
                logger.info(f"Retrying in {wait_time}s...")
                await asyncio.sleep(wait_time)
            except httpx.RequestError as e:
                last_exception = e
                logger.warning(f"Request error on attempt {attempt + 1}/{self.max_retries}: {e}")
                if attempt == self.max_retries - 1:
                    break
                wait_time = calculate_backoff_delay(attempt)
                logger.info(f"Retrying in {wait_time}s...")
                await asyncio.sleep(wait_time)

        logger.error(f"Request failed after {self.max_retries} attempts: {last_exception}")
        raise APIError(f"Request failed after {self.max_retries} attempts: {last_exception}")

    async def authenticate(self) -> AuthResponse:
        """Authenticate and get session ID.

        Returns:
            Authentication response

        Raises:
            AuthenticationError: If authentication fails
        """
        logger.info(f"Authenticating as {self.email}")
        url = f"{BASE_URL}{ENDPOINT_AUTHENTICATE}"
        params = {"email": self.email, "password": self.password}

        response = await self._request_with_retry("GET", url, params=params)
        auth_response = AuthResponse.from_dict(response.json())

        if not auth_response.is_success:
            logger.error(f"Authentication failed with error code: {auth_response.error_code}")
            raise AuthenticationError(
                f"Authentication failed with error code: {auth_response.error_code}"
            )

        self._session_id = auth_response.session_id
        self._thermostats_fetched = False  # Reset for new session
        logger.info(f"Authentication successful, session ID: {self._session_id[:8]}...")
        return auth_response

    async def _ensure_authenticated(self) -> None:
        """Ensure we have a valid session ID."""
        if not self._session_id:
            logger.debug("No session ID, authenticating...")
            await self.authenticate()

    async def get_thermostats(self) -> ThermostatsResponse:
        """Get all thermostats.

        Returns:
            Thermostats response with all groups and thermostats

        Raises:
            SessionExpiredError: If session is expired
            APIError: If request fails
        """
        await self._ensure_authenticated()

        logger.info("Fetching all thermostats")
        url = f"{BASE_URL}{ENDPOINT_THERMOSTATS}"
        params = {"sessionid": self._session_id}

        try:
            response = await self._request_with_retry("GET", url, params=params)
            thermostats_response = ThermostatsResponse.from_dict(response.json())
            total_count = sum(len(g.thermostats) for g in thermostats_response.groups)
            logger.info(
                f"Retrieved {total_count} thermostat(s) in "
                f"{len(thermostats_response.groups)} group(s)"
            )
            self._thermostats_fetched = True  # Mark that we've fetched thermostats
            return thermostats_response
        except SessionExpiredError:
            logger.warning("Session expired, re-authenticating...")
            self._session_id = None
            await self._ensure_authenticated()
            params = {"sessionid": self._session_id}
            response = await self._request_with_retry("GET", url, params=params)
            thermostats_response = ThermostatsResponse.from_dict(response.json())
            total_count = sum(len(g.thermostats) for g in thermostats_response.groups)
            logger.info(
                f"Retrieved {total_count} thermostat(s) in "
                f"{len(thermostats_response.groups)} group(s)"
            )
            self._thermostats_fetched = True  # Mark that we've fetched thermostats
            return thermostats_response

    async def get_thermostat(self, serial_number: str) -> Thermostat:
        """Get a specific thermostat by serial number.

        Args:
            serial_number: Thermostat serial number

        Returns:
            Thermostat data

        Raises:
            ThermostatNotFoundError: If thermostat is not found
            SessionExpiredError: If session is expired
            APIError: If request fails
        """
        logger.info(f"Fetching thermostat with serial number: {serial_number}")
        thermostats_response = await self.get_thermostats()
        thermostat = thermostats_response.get_thermostat(serial_number)

        if thermostat is None:
            logger.error(f"Thermostat {serial_number} not found")
            raise ThermostatNotFoundError(
                f"Thermostat with serial number {serial_number} not found"
            )

        logger.debug(f"Found thermostat: {thermostat.room} ({serial_number})")
        return thermostat

    async def update_thermostat(
        self,
        serial_number: str,
        thermostat: Thermostat,
    ) -> UpdateThermostatResponse:
        """Update thermostat settings.

        Args:
            serial_number: Thermostat serial number
            thermostat: Thermostat object with updated settings

        Returns:
            Update response

        Raises:
            SessionExpiredError: If session is expired
            APIError: If request fails
        """
        await self._ensure_authenticated()

        logger.info(f"Updating thermostat {serial_number} ({thermostat.room})")
        logger.debug(
            f"Update data: regulation_mode={thermostat.regulation_mode}, "
            f"manual_temp={thermostat.manual_temperature}"
        )
        url = f"{BASE_URL}{ENDPOINT_THERMOSTAT}"
        params = {"sessionId": self._session_id, "serialnumber": serial_number}

        try:
            response = await self._request_with_retry(
                "POST", url, params=params, json=thermostat.to_dict()
            )
            result = UpdateThermostatResponse.from_dict(response.json())
            if result.success:
                logger.info(f"Successfully updated thermostat {serial_number}")
            else:
                logger.warning(f"Update returned success=False for thermostat {serial_number}")
            return result
        except SessionExpiredError:
            logger.warning("Session expired during update, re-authenticating...")
            self._session_id = None
            await self._ensure_authenticated()
            params = {"sessionId": self._session_id, "serialnumber": serial_number}
            response = await self._request_with_retry(
                "POST", url, params=params, json=thermostat.to_dict()
            )
            result = UpdateThermostatResponse.from_dict(response.json())
            if result.success:
                logger.info(f"Successfully updated thermostat {serial_number}")
            else:
                logger.warning(f"Update returned success=False for thermostat {serial_number}")
            return result

    async def set_manual_temperature(
        self,
        serial_number: str,
        temperature_celsius: float,
    ) -> UpdateThermostatResponse:
        """Set manual temperature for a thermostat.

        Args:
            serial_number: Thermostat serial number
            temperature_celsius: Target temperature in Celsius

        Returns:
            Update response
        """
        from .models import celsius_to_temp

        logger.info(f"Setting manual temperature for {serial_number} to {temperature_celsius}°C")
        thermostat = await self.get_thermostat(serial_number)
        thermostat.manual_temperature = celsius_to_temp(temperature_celsius)
        thermostat.regulation_mode = RegulationMode.MANUAL
        logger.debug(f"Setting regulation mode to {RegulationMode.MANUAL} (manual)")
        return await self.update_thermostat(serial_number, thermostat)

    async def turn_off(self, serial_number: str) -> UpdateThermostatResponse:
        """Turn off a thermostat.

        Args:
            serial_number: Thermostat serial number

        Returns:
            Update response
        """
        logger.info(f"Turning off thermostat {serial_number}")
        thermostat = await self.get_thermostat(serial_number)
        thermostat.regulation_mode = RegulationMode.OFF
        logger.debug(f"Setting regulation mode to {RegulationMode.OFF} (off)")
        return await self.update_thermostat(serial_number, thermostat)

    async def start_boost(
        self, serial_number: str, end_time: datetime | None = None
    ) -> UpdateThermostatResponse:
        """Start boost mode on a thermostat.

        Boost mode (comfort mode) temporarily overrides the schedule with a higher
        temperature until the specified end time.

        Args:
            serial_number: Thermostat serial number
            end_time: When boost should end (defaults to 3 hours from now)

        Returns:
            Update response
        """
        logger.info(f"Starting boost on thermostat {serial_number}")
        thermostat = await self.get_thermostat(serial_number)

        # Default to 3 hours from now if not specified
        if end_time is None:
            end_time = datetime.now(timezone.utc) + timedelta(hours=3)

        # Format: DD-MM-YYYY HH:MM:SS+TZ:TZ
        formatted_time = end_time.strftime("%d-%m-%Y %H:%M:%S%z")
        # Add colon in timezone offset (e.g., +0000 -> +00:00)
        if len(formatted_time) >= 2:
            formatted_time = formatted_time[:-2] + ":" + formatted_time[-2:]

        thermostat.regulation_mode = RegulationMode.BOOST
        thermostat.comfort_end_time = formatted_time
        logger.debug(
            f"Setting regulation mode to {RegulationMode.BOOST} (boost) "
            f"until {formatted_time}"
        )
        return await self.update_thermostat(serial_number, thermostat)

    async def wait_for_notification(
        self, timeout: float = DEFAULT_NOTIFICATION_TIMEOUT
    ) -> Notification | None:
        """Wait for a thermostat change notification (long-polling).

        This method blocks until a change is detected or the timeout is reached.
        The API keeps the connection open until a thermostat state changes.

        Args:
            timeout: Request timeout in seconds (default 300s/5min)

        Returns:
            Notification object if a change was detected, None if timeout

        Raises:
            SessionExpiredError: If session is expired
            APIError: If request fails
        """
        await self._ensure_authenticated()

        # The notification endpoint requires thermostats to be fetched first
        # to "register" the session for notifications
        if not self._thermostats_fetched:
            logger.debug("Thermostats not yet fetched, fetching to initialize session...")
            await self.get_thermostats()

        logger.info("Waiting for notification...")
        url = f"{BASE_URL}{ENDPOINT_NOTIFICATION}"
        params = {"sessionId": self._session_id}

        try:
            # Use a temporary client with custom timeout for this long-polling request
            # For long-polling, we need to allow the entire request to take a long time
            # Set timeout to None to disable timeout, or use a very high value
            timeout_config = httpx.Timeout(timeout, connect=10.0)
            async with httpx.AsyncClient(timeout=timeout_config) as client:
                response = await client.request("GET", url, params=params)
                response.raise_for_status()
                notification_data = response.json()
                notification = Notification.from_dict(notification_data)
                logger.info(
                    f"Notification received: thermostat {notification.thermostat.serial_number}"
                )
                logger.debug(f"Notification data: {notification_data}")
                return notification
        except httpx.TimeoutException:
            logger.debug("Notification request timed out (no changes detected)")
            return None
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 401:
                logger.error("Session expired (401 Unauthorized)")
                raise SessionExpiredError("Session expired, please re-authenticate") from e
            logger.error(f"HTTP error {e.response.status_code} while waiting for notification")
            raise APIError(f"Notification request failed: {e}") from e
        except httpx.RequestError as e:
            logger.error(f"Request error while waiting for notification: {e}")
            raise APIError(f"Notification request failed: {e}") from e

    async def start_monitoring(
        self,
        callback: Callable[[Notification], Awaitable[None]],
        error_callback: Callable[[Exception], Awaitable[None]] | None = None,
        notification_timeout: float = DEFAULT_NOTIFICATION_TIMEOUT,
    ) -> asyncio.Task[None]:
        """Start monitoring for thermostat changes in a background task.

        The callback will be invoked whenever a thermostat change is detected.
        Monitoring continues until stop_monitoring() is called.

        Args:
            callback: Async function called with Notification when changes occur
            error_callback: Optional async function called when errors occur
            notification_timeout: Timeout for each notification request (default 300s)

        Returns:
            The monitoring task

        Raises:
            RuntimeError: If monitoring is already running
        """
        if self._monitoring_task and not self._monitoring_task.done():
            raise RuntimeError("Monitoring is already running")

        logger.info("Starting monitoring task")
        self._monitoring_stop_event.clear()
        self._monitoring_task = asyncio.create_task(
            self._monitoring_loop(callback, error_callback, notification_timeout),
            name="PentairMonitoring",
        )
        return self._monitoring_task

    async def stop_monitoring(self) -> None:
        """Stop the monitoring task."""
        if not self._monitoring_task or self._monitoring_task.done():
            logger.warning("Monitoring is not running")
            return

        logger.info("Stopping monitoring task")
        self._monitoring_stop_event.set()

        # Cancel the task
        self._monitoring_task.cancel()
        try:
            await asyncio.wait_for(self._monitoring_task, timeout=5.0)
        except TimeoutError:
            logger.warning("Monitoring task did not stop gracefully")
        except asyncio.CancelledError:
            logger.info("Monitoring task cancelled")

        self._monitoring_task = None

    async def _monitoring_loop(
        self,
        callback: Callable[[Notification], Awaitable[None]],
        error_callback: Callable[[Exception], Awaitable[None]] | None,
        notification_timeout: float,
    ) -> None:
        """Internal monitoring loop that runs in a background task."""
        logger.debug("Monitoring loop started")
        try:
            while not self._monitoring_stop_event.is_set():
                try:
                    notification = await self.wait_for_notification(timeout=notification_timeout)
                    if notification is not None:  # Only callback if there's actual data
                        await callback(notification)
                except SessionExpiredError:
                    logger.warning("Session expired during monitoring, re-authenticating...")
                    try:
                        self._session_id = None
                        await self._ensure_authenticated()
                    except Exception as auth_error:
                        logger.error(f"Re-authentication failed: {auth_error}")
                        if error_callback:
                            await error_callback(auth_error)
                        break
                except asyncio.CancelledError:
                    logger.debug("Monitoring loop cancelled")
                    raise
                except Exception as e:
                    logger.error(f"Error in monitoring loop: {e}")
                    if error_callback:
                        await error_callback(e)
                    # Continue monitoring unless stop event is set
                    if not self._monitoring_stop_event.is_set():
                        await asyncio.sleep(1)  # Brief pause before retrying
        except asyncio.CancelledError:
            logger.debug("Monitoring loop cancelled")
        finally:
            logger.debug("Monitoring loop ended")

    async def monitor_changes(
        self,
        timeout_per_request: float = DEFAULT_NOTIFICATION_TIMEOUT,
        max_iterations: int | None = None,
    ) -> AsyncIterator[Notification]:
        """Async generator that yields notifications as they arrive.

        This is a convenient way to monitor changes using async iteration.
        To stop monitoring, simply break out of the loop.

        Args:
            timeout_per_request: Timeout for each notification request (default 300s)
            max_iterations: Optional maximum number of notifications to yield

        Yields:
            Notification objects

        Example:
            async for notification in client.monitor_changes():
                print(f"Change detected: {notification.thermostat.serial_number}")
                if some_condition:
                    break
        """
        logger.info("Starting change monitoring (async iterator)")
        iteration = 0

        try:
            while max_iterations is None or iteration < max_iterations:
                try:
                    notification = await self.wait_for_notification(timeout=timeout_per_request)
                    if notification is not None:  # Only yield if there's actual data
                        yield notification
                        iteration += 1
                except SessionExpiredError:
                    logger.warning("Session expired during monitoring, re-authenticating...")
                    self._session_id = None
                    await self._ensure_authenticated()
                except asyncio.CancelledError:
                    logger.debug("Change monitoring cancelled")
                    break
                except Exception as e:
                    logger.error(f"Error in change monitoring: {e}")
                    # Re-raise to let caller handle
                    raise
        finally:
            logger.info("Change monitoring ended")

    async def close(self) -> None:
        """Close the HTTP client and stop monitoring if running."""
        await self.stop_monitoring()
        if self._client:
            await self._client.aclose()
            self._client = None
