"""HTTP client for MoEngage APIs.

Implements:
- Basic Authentication
- Retry logic with exponential backoff
- Rate limit handling
- Structured error responses
- Connection pooling via httpx
"""

import base64
import logging
from typing import Any

import httpx

from .config import Settings
from .models import (
    CampaignStats,
    ErrorResponse,
    PerformanceStats,
    StatsRequest,
    StatsResponse,
    VariationStats,
)

logger = logging.getLogger(__name__)


class MoEngageAPIError(Exception):
    """Base exception for MoEngage API errors."""

    def __init__(
        self,
        message: str,
        error_code: str = "API_ERROR",
        status_code: int | None = None,
        details: dict[str, Any] | None = None,
        retry_after: int | None = None,
    ):
        super().__init__(message)
        self.error_code = error_code
        self.status_code = status_code
        self.details = details
        self.retry_after = retry_after

    def to_error_response(self) -> ErrorResponse:
        """Convert to structured error response."""
        return ErrorResponse(
            error_code=self.error_code,
            error_message=str(self),
            details=self.details,
            retry_after=self.retry_after,
        )


class RateLimitError(MoEngageAPIError):
    """Raised when API rate limit is exceeded."""

    def __init__(self, retry_after: int = 60):
        super().__init__(
            message=f"Rate limit exceeded. Retry after {retry_after} seconds.",
            error_code="RATE_LIMIT_EXCEEDED",
            status_code=429,
            retry_after=retry_after,
        )


class AuthenticationError(MoEngageAPIError):
    """Raised when authentication fails."""

    def __init__(self, message: str = "Invalid credentials"):
        super().__init__(
            message=message,
            error_code="AUTHENTICATION_FAILED",
            status_code=401,
        )


class MoEngageClient:
    """HTTP client for MoEngage Stats API.

    Handles authentication, retries, and error handling.
    Uses connection pooling for efficient HTTP requests.
    """

    def __init__(self, settings: Settings):
        """Initialize the client with settings.

        Args:
            settings: Application settings with credentials and configuration.
        """
        self.settings = settings
        self._client: httpx.AsyncClient | None = None

    def _get_auth_header(self) -> str:
        """Generate Basic Auth header value.

        Returns:
            Base64-encoded authorization string.
        """
        credentials = f"{self.settings.workspace_id}:{self.settings.campaign_reports_api_key}"
        encoded = base64.b64encode(credentials.encode()).decode()
        return f"Basic {encoded}"

    def _get_headers(self) -> dict[str, str]:
        """Get request headers including authentication.

        Returns:
            Dictionary of HTTP headers.
        """
        return {
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": self._get_auth_header(),
            "MOE-APPKEY": self.settings.workspace_id,
        }

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or create the HTTP client.

        Returns:
            Configured async HTTP client.
        """
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(
                timeout=httpx.Timeout(self.settings.timeout),
                limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
            )
        return self._client

    async def close(self) -> None:
        """Close the HTTP client and release resources."""
        if self._client and not self._client.is_closed:
            await self._client.aclose()
            self._client = None

    async def _make_request(
        self,
        method: str,
        url: str,
        json_data: dict[str, Any] | None = None,
    ) -> tuple[dict[str, Any], dict[str, str]]:
        """Make an HTTP request with retry logic.

        Args:
            method: HTTP method (GET, POST, etc.)
            url: Request URL
            json_data: JSON body for POST requests

        Returns:
            Tuple of (response_json, response_headers)

        Raises:
            MoEngageAPIError: On API errors
            RateLimitError: When rate limit is exceeded
            AuthenticationError: On auth failures
        """
        client = await self._get_client()
        last_error: Exception | None = None

        for attempt in range(self.settings.max_retries + 1):
            try:
                logger.debug(
                    "Making request attempt %d/%d: %s %s",
                    attempt + 1,
                    self.settings.max_retries + 1,
                    method,
                    url,
                )

                response = await client.request(
                    method=method,
                    url=url,
                    headers=self._get_headers(),
                    json=json_data,
                )

                # Handle rate limiting
                if response.status_code == 429:
                    retry_after = int(response.headers.get("Retry-After", "60"))
                    raise RateLimitError(retry_after=retry_after)

                # Handle authentication errors
                if response.status_code == 401:
                    raise AuthenticationError(
                        "Invalid workspace ID or API key. "
                        "Please verify your credentials in MoEngage Dashboard > Settings > Account > APIs"
                    )

                if response.status_code == 403:
                    raise AuthenticationError(
                        "Access forbidden. Ensure your API key has permissions for the Stats API."
                    )

                # Handle other client errors
                if 400 <= response.status_code < 500:
                    error_body = response.json() if response.content else {}
                    # Handle case where error_body is a list
                    if isinstance(error_body, list):
                        error_msg = str(error_body)
                        error_body = {"errors": error_body}
                    else:
                        error_msg = error_body.get("message", f"Client error: {response.status_code}")
                    raise MoEngageAPIError(
                        message=error_msg,
                        error_code=f"CLIENT_ERROR_{response.status_code}",
                        status_code=response.status_code,
                        details=error_body,
                    )

                # Handle server errors (retry)
                if response.status_code >= 500:
                    raise MoEngageAPIError(
                        message=f"Server error: {response.status_code}",
                        error_code=f"SERVER_ERROR_{response.status_code}",
                        status_code=response.status_code,
                    )

                # Success
                return response.json(), dict(response.headers)

            except (httpx.TimeoutException, httpx.NetworkError) as e:
                last_error = MoEngageAPIError(
                    message=f"Network error: {e!s}",
                    error_code="NETWORK_ERROR",
                    details={"exception": type(e).__name__},
                )
                logger.warning(
                    "Request failed (attempt %d/%d): %s",
                    attempt + 1,
                    self.settings.max_retries + 1,
                    e,
                )
                if attempt == self.settings.max_retries:
                    raise last_error from e

            except RateLimitError:
                # Don't retry rate limits
                raise

            except MoEngageAPIError as e:
                if e.status_code and e.status_code >= 500:
                    # Retry server errors
                    last_error = e
                    logger.warning(
                        "Server error (attempt %d/%d): %s",
                        attempt + 1,
                        self.settings.max_retries + 1,
                        e,
                    )
                    if attempt == self.settings.max_retries:
                        raise
                else:
                    # Don't retry client errors
                    raise

        # Should not reach here, but just in case
        if last_error:
            raise last_error
        raise MoEngageAPIError("Unknown error occurred", error_code="UNKNOWN_ERROR")

    def _parse_performance_stats(self, data: dict[str, Any]) -> PerformanceStats:
        """Parse performance stats from API response.

        Args:
            data: Raw performance stats dictionary

        Returns:
            Parsed PerformanceStats model
        """
        return PerformanceStats(
            attempted=data.get("attempted", 0),
            sent=data.get("sent", 0),
            failed=data.get("failed", 0),
            impression=data.get("impression", 0),
            click=data.get("click", 0),
            ctr=data.get("ctr", 0.0),
            delivery_rate=data.get("delivery_rate", 0.0),
            sent_rate=data.get("sent_rate", 0.0),
            failure_rate=data.get("failure_rate", 0.0),
        )

    def _parse_campaign_stats(self, campaign_id: str, data: Any) -> CampaignStats:
        """Parse campaign stats from API response.

        The API returns data in a nested structure:
        data[campaign_id] = [{ platforms: { ALL_PLATFORMS: { locales: { all_locale: { variations: {...} } } } } }]

        Args:
            campaign_id: The campaign ID
            data: Raw campaign data (can be list or dict)

        Returns:
            Parsed CampaignStats model
        """
        # Handle case where data is a list (API returns list of platform breakdowns)
        if isinstance(data, list) and len(data) > 0:
            data = data[0]  # Take first item

        if not isinstance(data, dict):
            logger.warning("Unexpected campaign data format: %s", type(data))
            return CampaignStats(campaign_id=campaign_id)

        # Navigate the nested structure to find ALL_PLATFORMS stats
        performance_stats = {}
        try:
            platforms = data.get("platforms", {})
            # Prefer ALL_PLATFORMS, fallback to first available platform
            platform_data = platforms.get("ALL_PLATFORMS") or next(iter(platforms.values()), {})
            locales = platform_data.get("locales", {})
            # Prefer all_locale, fallback to first available locale
            locale_data = locales.get("all_locale") or next(iter(locales.values()), {})
            variations_data = locale_data.get("variations", {})
            all_variations = variations_data.get("all_variations", {})
            performance_stats = all_variations.get("performance_stats", {})
        except (AttributeError, StopIteration) as e:
            logger.warning("Error parsing nested structure: %s", e)

        # Parse variations if available
        variations = []
        try:
            variations_dict = locale_data.get("variations", {}) if 'locale_data' in dir() else {}
            for var_name, var_data in variations_dict.items():
                if var_name not in ("all_variations", "control_group") and isinstance(var_data, dict):
                    var_perf = var_data.get("performance_stats", {})
                    variations.append(
                        VariationStats(
                            variation_id=var_name,
                            variation_name=var_name,
                            performance_stats=self._parse_performance_stats(var_perf),
                        )
                    )
        except Exception as e:
            logger.debug("Could not parse variations: %s", e)

        # Get control group stats
        control_group_stats = None
        try:
            control_data = variations_data.get("control_group", {})
            if control_data:
                control_perf = control_data.get("performance_stats")
                if control_perf:
                    control_group_stats = self._parse_performance_stats(control_perf)
        except Exception:
            pass

        return CampaignStats(
            campaign_id=campaign_id,
            campaign_name=data.get("campaign_name"),
            campaign_type=data.get("campaign_type"),
            channel=data.get("channel"),
            all_variations=self._parse_performance_stats(performance_stats),
            variations=variations,
            global_control_group=control_group_stats,
            campaign_control_group=None,
        )

    async def get_campaign_stats(self, request: StatsRequest) -> StatsResponse:
        """Fetch campaign statistics from MoEngage Stats API.

        Args:
            request: Validated stats request

        Returns:
            Structured stats response

        Raises:
            MoEngageAPIError: On API errors
        """
        logger.info(
            "Fetching stats for %d campaigns: %s to %s",
            len(request.campaign_ids),
            request.start_date,
            request.end_date,
        )

        try:
            response_data, headers = await self._make_request(
                method="POST",
                url=self.settings.stats_api_url,
                json_data=request.to_api_request(),
            )

            # Log full response for debugging (DEBUG level to avoid exposing sensitive data)
            logger.debug("Full API response type: %s", type(response_data))
            logger.debug("Full API response: %s", response_data)

            # Parse campaigns from response
            campaigns = []

            # Handle case where response_data itself is a list
            if isinstance(response_data, list):
                logger.debug("Response is a list, processing directly")
                data_section = response_data
            else:
                data_section = response_data.get("data", {}) if isinstance(response_data, dict) else {}

            # Log raw response for debugging
            logger.debug("Raw API response: %s", response_data)

            # Handle different response formats
            if isinstance(data_section, list):
                # Response is a list of campaign stats
                for item in data_section:
                    if isinstance(item, dict):
                        cid = item.get("campaign_id") or item.get("id")
                        if cid:
                            campaigns.append(self._parse_campaign_stats(cid, item))
            elif isinstance(data_section, dict):
                # Response is a dict keyed by campaign_id
                for campaign_id in request.campaign_ids:
                    campaign_data = data_section.get(campaign_id, {})
                    if campaign_data:
                        campaigns.append(self._parse_campaign_stats(campaign_id, campaign_data))
                    else:
                        # Campaign not found in response - add empty stats
                        campaigns.append(
                            CampaignStats(
                                campaign_id=campaign_id,
                                campaign_name=None,
                            )
                        )
            else:
                logger.warning("Unexpected data format: %s", type(data_section))
                # Return empty campaigns list
                for campaign_id in request.campaign_ids:
                    campaigns.append(
                        CampaignStats(
                            campaign_id=campaign_id,
                            campaign_name=None,
                        )
                    )

            # Extract rate limit info if available
            rate_limit_remaining = None
            if "X-RateLimit-Remaining" in headers:
                try:
                    rate_limit_remaining = int(headers["X-RateLimit-Remaining"])
                except (ValueError, TypeError):
                    pass

            return StatsResponse(
                request_id=request.request_id,
                success=True,
                campaigns=campaigns,
                rate_limit_remaining=rate_limit_remaining,
            )

        except MoEngageAPIError as e:
            logger.error("Stats API error: %s", e)
            return StatsResponse(
                request_id=request.request_id,
                success=False,
                error_message=str(e),
            )

    def _parse_campaign_details(self, data: dict[str, Any]) -> "CampaignDetails":
        """Parse campaign details from Campaign Search API response.

        Args:
            data: Raw campaign data dictionary

        Returns:
            Parsed CampaignDetails model
        """
        from .models import (
            BasicCampaignDetails,
            CampaignContentVariation,
            CampaignDetails,
        )

        # Parse basic details
        basic = data.get("basic_details", {})
        basic_details = BasicCampaignDetails(
            name=basic.get("name"),
            content_type=basic.get("content_type"),
            team=basic.get("team"),
            tags=basic.get("tags", []),
            subscription_category=basic.get("subscription_category"),
        )

        # Parse variations
        variations = []
        campaign_content = data.get("campaign_content", {})
        variation_details = campaign_content.get("variation_details", [])
        if isinstance(variation_details, list):
            for var in variation_details:
                variations.append(
                    CampaignContentVariation(
                        variation_id=var.get("variation_id"),
                        variation_name=var.get("variation_name"),
                        subject=var.get("subject"),
                        preview_text=var.get("preview_text"),
                        from_name=var.get("from_name"),
                        from_email=var.get("from_email"),
                        reply_to=var.get("reply_to"),
                        title=var.get("title"),
                        message=var.get("message"),
                    )
                )

        # Parse connector info - can be dict or string
        connector = data.get("connector", {})
        connector_type = None
        connector_name = None
        if isinstance(connector, dict):
            connector_type = connector.get("connector_type")
            connector_name = connector.get("connector_name")
        elif isinstance(connector, str):
            # Sometimes connector is just a string name
            connector_name = connector

        return CampaignDetails(
            campaign_id=data.get("campaign_id") or data.get("_id", ""),
            channel=data.get("channel"),
            platform=data.get("platform"),
            created_by=data.get("created_by"),
            created_at=data.get("created_at"),
            updated_at=data.get("updated_at"),
            campaign_delivery_type=data.get("campaign_delivery_type"),
            campaign_status=data.get("campaign_status"),
            campaign_start_time=data.get("campaign_start_time"),
            campaign_end_time=data.get("campaign_end_time"),
            basic_details=basic_details,
            variations=variations,
            utm_params=data.get("utm_params") if isinstance(data.get("utm_params"), dict) else {},
            connector_type=connector_type,
            connector_name=connector_name,
        )

    async def search_campaigns(
        self, request: "CampaignSearchRequest"
    ) -> "CampaignSearchResponse":
        """Search campaigns by channel (Email, Push, SMS, etc.).

        Args:
            request: Campaign search request with filters

        Returns:
            CampaignSearchResponse with matching campaigns

        Raises:
            MoEngageAPIError: On API errors
        """
        from .models import CampaignSearchRequest, CampaignSearchResponse

        channels_str = (
            [c.value for c in request.channels] if request.channels else "any"
        )
        logger.info(
            "Searching campaigns: channels=%s, limit=%d, page=%d, campaign_id=%s",
            channels_str,
            request.limit,
            request.page,
            request.campaign_id,
        )

        try:
            api_request = request.to_api_request()
            logger.debug("API request payload: %s", api_request)

            response_data, headers = await self._make_request(
                method="POST",
                url=f"{self.settings.api_base_url}/core-services/v1/campaigns/search",
                json_data=api_request,
            )

            logger.debug("Campaign search response type: %s", type(response_data))
            logger.debug("Campaign search response: %s", response_data)

            # Parse campaigns from response
            campaigns = []

            # Handle different response formats
            # The API returns a direct list, not {"data": [...]}
            if isinstance(response_data, list):
                data_section = response_data
            elif isinstance(response_data, dict):
                # Could be {"data": [...]} or a single campaign object
                data_section = response_data.get("data", response_data)
            else:
                data_section = []

            logger.debug("Data section type: %s", type(data_section))

            if isinstance(data_section, list):
                for item in data_section:
                    if isinstance(item, dict):
                        campaigns.append(self._parse_campaign_details(item))
                    else:
                        logger.warning("Skipping non-dict item: %s (type: %s)", item, type(item))
            elif isinstance(data_section, dict):
                # Could be a single campaign object or dict keyed by campaign_id
                # Check if it looks like a single campaign (has campaign_id or _id)
                if "campaign_id" in data_section or "_id" in data_section:
                    campaigns.append(self._parse_campaign_details(data_section))
                else:
                    # Dict format - iterate values
                    for key, item in data_section.items():
                        if isinstance(item, dict):
                            campaigns.append(self._parse_campaign_details(item))
                        else:
                            logger.warning("Skipping non-dict value for key %s: %s", key, type(item))

            # Extract pagination info if available (depends on response format)
            if isinstance(response_data, dict) and "data" in response_data:
                total_campaigns = response_data.get("total_campaigns", len(campaigns))
                current_page = response_data.get("current_page", request.page)
                total_pages = response_data.get("total_pages", 1)
            else:
                # Direct list response - no pagination info
                total_campaigns = len(campaigns)
                current_page = request.page
                total_pages = 1

            return CampaignSearchResponse(
                request_id=request.request_id,
                success=True,
                total_campaigns=total_campaigns,
                current_page=current_page,
                total_pages=total_pages,
                campaigns=campaigns,
            )

        except MoEngageAPIError as e:
            logger.error("Campaign search API error: %s", e)
            return CampaignSearchResponse(
                request_id=request.request_id,
                success=False,
                error_message=str(e),
            )
