"""
Methods for the events namespace

https://docs.github.com/en/rest/reference/activity#events
"""

from __future__ import annotations

import asyncio
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Literal, TypedDict
from uuid import uuid4

from ..const import LOGGER, GitHubRequestKwarg, RepositoryType
from ..exceptions import (
    GitHubAuthenticationException,
    GitHubException,
    GitHubNotFoundException,
    GitHubNotModifiedException,
    GitHubPermissionException,
)
from ..helpers import repository_full_name
from ..models.events import GitHubEventModel
from .base import BaseNamespace

if TYPE_CHECKING:
    from ..client import GitHubClient

_DEFAULT_BACKOFF = 300
_DEFAULT_POLL = 60


class _GitHubEventsBaseNamespace(BaseNamespace):
    """Methods for the events namespace"""

    def __init__(
        self,
        client: GitHubClient,
        space: Literal["users"] | Literal["repos"] | Literal["orgs"],
    ) -> None:
        super().__init__(client)
        self._space = space
        self._subscriptions: Dict[str, asyncio.Task[None]] = {}

    @staticmethod
    async def _wait(wait_time: float) -> None:
        """Wait for x seconds"""
        await asyncio.sleep(wait_time)

    async def subscribe(
        self,
        name: str,
        event_callback: Callable[[GitHubEventModel], Awaitable[None]],
        *,
        error_callback: Callable[[GitHubException], Awaitable[None]] | None = None,
        **kwargs: Dict[GitHubRequestKwarg, Any],
    ) -> str:
        """
         Subscribe to an event stream.
         This returns an ID you can use with the unsubscribe method to stop listening for events.

         **Arguments**:

         `name`

         The name to return evets from, example "octocat/hello-world"

         `event_callback`

         An async function that will be called when new events come in,
         the event will be passed as the first argument.

         `error_callback` (Optional)

         An async function that will be called when errors occur,
         the exception that where raised will be passed.

        https://docs.github.com/en/rest/reference/activity#list-public-events
        """
        subscription_id = str(uuid4())

        async def _subscriber():
            _last_etag: str | None = None
            _poll_time: int = 60
            _target_time = datetime.now(tz=timezone.utc).replace(tzinfo=None).isoformat()
            LOGGER.debug("Starting event subscription for github.com/%s", name)
            subscription_task = self._subscriptions[subscription_id]
            while not subscription_task.cancelled():
                try:
                    response = await self._client.async_call_api(
                        endpoint=f"/{self._space}/{name}/events",
                        etag=_last_etag,
                        **kwargs,
                    )
                except GitHubNotModifiedException:
                    await self._wait(_poll_time)
                    continue
                except (
                    GitHubAuthenticationException,
                    GitHubNotFoundException,
                    GitHubPermissionException,
                ) as err:
                    await error_callback(err)
                    break
                except GitHubException as err:
                    if error_callback is not None:
                        await error_callback(err)
                    await self._wait(_DEFAULT_BACKOFF)
                    continue
                else:
                    _last_etag = response.headers.etag
                    _poll_time = (
                        int(response.headers.x_poll_interval)
                        if response.headers.x_poll_interval
                        else _DEFAULT_POLL
                    )

                    response.data = [
                        GitHubEventModel(event) for event in reversed(response.data or [])
                    ]

                    for event in response.data:
                        if event.created_at < _target_time:
                            continue
                        _target_time = event.created_at

                        LOGGER.debug("New %s for %s", event.type, name)
                        try:
                            await event_callback(event)
                        except Exception as err:
                            if error_callback is not None:
                                await error_callback(GitHubException(err))

                await self._wait(_poll_time)

            LOGGER.debug("Stopping event subscription for github.com/%s", name)
            self.unsubscribe(subscription_id=subscription_id)

        self._subscriptions[subscription_id] = asyncio.create_task(_subscriber())

        return subscription_id

    def unsubscribe(
        self,
        *,
        subscription_id: str | None = None,
    ) -> list[asyncio.Task]:
        """
        Unsubscribe to an event stream

        **Arguments**:

        `subscription_id` (Optional)

        The ID you got when you subscribed, if omitted all active subscriptions will be stopped.
        """

        def _cancel_subscription(subid: str, task: asyncio.Task[None]) -> asyncio.Task:
            if not task.cancelled():
                task.cancel()
            del self._subscriptions[subid]
            return task

        return [
            _cancel_subscription(subid=subid, task=task)
            for subid in (
                [subscription_id]
                if subscription_id is not None
                else list(self._subscriptions.keys())
            )
            if (task := self._subscriptions.get(subid)) is not None
        ]

    async def unsubscribe_all(self) -> None:
        """Unsubscribe and wait for all subscriptions to be done."""
        if tasks := self.unsubscribe():
            await asyncio.wait(tasks)


class GitHubEventsReposNamespace(_GitHubEventsBaseNamespace):
    """Methods for the repository events namespace"""

    def __init__(self, client: GitHubClient) -> None:
        super().__init__(client, space="repos")

    async def subscribe(
        self,
        repository: RepositoryType,
        event_callback: Callable[[GitHubEventModel], Awaitable[None]],
        *,
        error_callback: Callable[[], Awaitable[None]] | None = None,
        **kwargs: Dict[GitHubRequestKwarg, Any],
    ) -> str:
        """
         Subscribe to an event stream.
         This returns an ID you can use with the unsubscribe method to stop listening for events.

         **Arguments**:

         `repository`

         The repository to return evets from, example "octocat/hello-world"

         `event_callback`

         An async function that will be called when new events come in,
         the event will be passed as the first argument.

         `error_callback` (Optional)

         An async function that will be called when errors occur,
         the exception that where raised will be passed.

        https://docs.github.com/en/rest/reference/activity#list-repository-events
        """
        return await super().subscribe(
            name=repository_full_name(repository),
            event_callback=event_callback,
            error_callback=error_callback,
            **kwargs,
        )
