"""Shared base classes for Meemo API service modules.

Provides common validation, URL construction, error constants, and parameter
building used by both sync and async variants of each service module.

Authors:
    GDP Labs

References:
    https://gdplabs.gitbook.io/meemo/resources/external-api-documentation
"""

import os
from datetime import datetime
from typing import Any

from meemo.models import (
    CreateMeetingBody,
    CreateMeetingCalendarEvent,
    CreateWebhookBody,
    MeetingListParams,
)

_ERR_EXPECTED_DICT = "Expected dict response from API"
_ERR_EXPECTED_LIST = "Expected list response from API"


class _BaseService:
    """Base class for sync Meemo API service modules."""

    def __init__(self, client):
        """Initialize the service.

        Args:
            client: MeemoClient instance.
        """
        self._client = client

    def _prepare_headers(self, extra_headers: dict[str, str] | None = None) -> dict[str, str]:
        """Prepare headers for the API request (delegates to client)."""
        return self._client._prepare_headers(extra_headers)

    def _make_request(
        self,
        method: str,
        url: str,
        headers: dict[str, str],
        json_data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> dict[str, Any] | list[dict[str, Any]]:
        """Make an HTTP request (delegates to client)."""
        return self._client._make_request(
            method=method,
            url=url,
            headers=headers,
            json_data=json_data,
            params=params,
        )


class _AsyncBaseService:
    """Base class for async Meemo API service modules."""

    def __init__(self, client):
        """Initialize the service.

        Args:
            client: AsyncMeemoClient instance.
        """
        self._client = client

    async def _prepare_headers(self, extra_headers: dict[str, str] | None = None) -> dict[str, str]:
        """Prepare headers for the API request (delegates to client)."""
        return await self._client._prepare_headers(extra_headers)

    async def _make_request(
        self,
        method: str,
        url: str,
        headers: dict[str, str],
        json_data: dict[str, Any] | None = None,
        params: dict[str, Any] | None = None,
    ) -> dict[str, Any] | list[dict[str, Any]]:
        """Make an async HTTP request (delegates to client)."""
        return await self._client._make_request(
            method=method,
            url=url,
            headers=headers,
            json_data=json_data,
            params=params,
        )


class _BaseMeetingsMixin:
    """Shared logic for Meetings and AsyncMeetings."""

    _ERR_MEETING_ID_POSITIVE = "meeting_id must be a positive integer"
    _ERR_ORG_ID_POSITIVE = "organization_id must be a positive integer"
    _ERR_SPEAKER_COUNT = "speaker_count must be >= 1"
    _ERR_BOT_FILE_EXCLUSIVE = "bot_meeting_url and file_url are mutually exclusive"
    _ERR_BOT_JOIN_AT_REQUIRES_URL = "bot_join_at requires bot_meeting_url"

    @staticmethod
    def _validate_meeting_id(meeting_id: int) -> None:
        """Raise ValueError if meeting_id is not a positive integer."""
        if not meeting_id or meeting_id <= 0:
            raise ValueError(_BaseMeetingsMixin._ERR_MEETING_ID_POSITIVE)

    @staticmethod
    def _validate_create_meeting_params(
        organization_id: int,
        speaker_count: int | None,
        bot_meeting_url: str | None,
        bot_join_at: str | None,
        file_url: str | None,
    ) -> None:
        """Validate parameters for create_meeting."""
        if not organization_id or organization_id <= 0:
            raise ValueError(_BaseMeetingsMixin._ERR_ORG_ID_POSITIVE)
        if speaker_count is not None and speaker_count < 1:
            raise ValueError(_BaseMeetingsMixin._ERR_SPEAKER_COUNT)
        if bot_meeting_url and file_url:
            raise ValueError(_BaseMeetingsMixin._ERR_BOT_FILE_EXCLUSIVE)
        if bot_join_at and not bot_meeting_url:
            raise ValueError(_BaseMeetingsMixin._ERR_BOT_JOIN_AT_REQUIRES_URL)

    def _meeting_url(self, path: str = "") -> str:
        """Build a meeting API URL."""
        return f"{self._client.base_url}/api/v1/external/meeting/{path}"

    def _topics_url(self) -> str:
        """Build the topics API URL."""
        return f"{self._client.base_url}/api/v1/external/topics/"

    @staticmethod
    def _build_list_params(
        organization_id: int | None = None,
        title: str | None = None,
        participants: str | None = None,
        created_after: str | None = None,
        created_before: str | None = None,
        summary_complete: bool | None = None,
        from_calendar: bool | None = None,
        page: int | None = None,
        size: int | None = None,
    ) -> dict[str, Any] | None:
        """Build query params dict for list_meetings."""
        list_params = MeetingListParams(
            organization_id=organization_id,
            title=title,
            participants=participants,
            created_after=created_after,
            created_before=created_before,
            summary_complete=summary_complete,
            from_calendar=from_calendar,
            page=page,
            size=size,
        )
        return list_params.to_query_params() or None

    @staticmethod
    def _build_create_body(
        organization_id: int,
        title: str | None = None,
        start_time: str | None = None,
        user_token: str | None = None,
        topic_id: int | None = None,
        participants: str | None = None,
        bot_meeting_url: str | None = None,
        bot_join_at: str | None = None,
        file_url: str | None = None,
        speaker_count: int | None = None,
        calendar_event: CreateMeetingCalendarEvent | None = None,
    ) -> dict[str, Any]:
        """Build the JSON body for create_meeting."""
        body = CreateMeetingBody(
            organization_id=organization_id,
            title=title,
            start_time=start_time,
            user_token=user_token,
            topic_id=topic_id,
            participants=participants,
            bot_meeting_url=bot_meeting_url,
            bot_join_at=bot_join_at,
            file_url=file_url,
            speaker_count=speaker_count,
            calendar_event=calendar_event,
        )
        return body.model_dump(exclude_none=True)


class _BaseWebhooksMixin:
    """Shared logic for Webhooks and AsyncWebhooks."""

    _ERR_WEBHOOK_ID_POSITIVE = "webhook_id must be a positive integer"
    _ERR_EVENT_ID_POSITIVE = "event_id must be a positive integer"

    @staticmethod
    def _validate_webhook_id(webhook_id: int) -> None:
        """Raise ValueError if webhook_id is not a positive integer."""
        if not webhook_id or webhook_id <= 0:
            raise ValueError(_BaseWebhooksMixin._ERR_WEBHOOK_ID_POSITIVE)

    @staticmethod
    def _validate_event_id(event_id: int) -> None:
        """Raise ValueError if event_id is not a positive integer."""
        if not event_id or event_id <= 0:
            raise ValueError(_BaseWebhooksMixin._ERR_EVENT_ID_POSITIVE)

    def _webhooks_url(self, path: str = "") -> str:
        """Build a webhooks API URL."""
        return f"{self._client.base_url}/api/v1/external/webhooks/{path}"

    def _webhook_events_url(self, path: str = "") -> str:
        """Build a webhook-events API URL."""
        return f"{self._client.base_url}/api/v1/external/webhook-events/{path}"

    @staticmethod
    def _build_create_webhook_body(
        target_url: str,
        events: list[str],
        organization_ids: list[int],
        description: str | None = None,
    ) -> dict[str, Any]:
        """Build the JSON body for create_webhook."""
        body = CreateWebhookBody(
            target_url=target_url,
            events=events,
            organization_ids=organization_ids,
            description=description,
        )
        return body.model_dump(exclude_none=True)


class _BaseClient:
    """Shared initialization for MeemoClient and AsyncMeemoClient."""

    def __init__(
        self,
        client_id: str | None = None,
        client_secret: str | None = None,
        base_url: str | None = None,
        timeout: float = 60.0,
        default_headers: dict[str, str] | None = None,
    ):
        """Initialize the client.

        Args:
            client_id (str | None): OAuth2 client ID. If not provided,
                will try to get from MEEMO_CLIENT_ID environment variable.
            client_secret (str | None): OAuth2 client secret. If not provided,
                will try to get from MEEMO_CLIENT_SECRET environment variable.
            base_url (str | None): Base URL of the Meemo instance. If not provided,
                will try to get from MEEMO_BASE_URL environment variable.
            timeout (float): Request timeout in seconds.
            default_headers (dict[str, str] | None): Default headers to include in all requests.
                These will be merged with any extra_headers provided to individual methods.

        Raises:
            ValueError: If client_id, client_secret, or base_url is not provided
                and not set via environment variables.
        """
        self.client_id = client_id or os.getenv("MEEMO_CLIENT_ID")
        if not self.client_id:
            raise ValueError(
                "Client ID is required. Provide it via 'client_id' parameter or "
                "'MEEMO_CLIENT_ID' environment variable."
            )

        self.client_secret = client_secret or os.getenv("MEEMO_CLIENT_SECRET")
        if not self.client_secret:
            raise ValueError(
                "Client secret is required. Provide it via 'client_secret' parameter or "
                "'MEEMO_CLIENT_SECRET' environment variable."
            )

        self.base_url = base_url or os.getenv("MEEMO_BASE_URL")
        if not self.base_url:
            raise ValueError(
                "Base URL is required. Provide it via 'base_url' parameter or "
                "'MEEMO_BASE_URL' environment variable."
            )

        # Remove trailing slash and /api suffix for consistent URL construction
        self.base_url = self.base_url.rstrip("/")
        if self.base_url.endswith("/api"):
            self.base_url = self.base_url[:-4]

        self.timeout = timeout
        self.default_headers = default_headers or {}

        # Token state
        self._access_token: str | None = None
        self._token_expires_at: datetime | None = None


class _BaseUsageMixin:
    """Shared logic for Usage and AsyncUsage."""

    def _usage_url(self) -> str:
        """Build the usage API URL."""
        return f"{self._client.base_url}/api/v1/external/usage/"

    @staticmethod
    def _build_usage_params(
        organization_id: int | None = None,
        start_date: str | None = None,
        end_date: str | None = None,
    ) -> dict[str, Any] | None:
        """Build query params dict for get_usage."""
        params: dict[str, Any] = {}
        if organization_id is not None:
            params["organization_id"] = organization_id
        if start_date is not None:
            params["start_date"] = start_date
        if end_date is not None:
            params["end_date"] = end_date
        return params or None
