"""
tasks.py
Implements base classes for background tasks, mostly running periodically at certain intervals.
"""

from abc import ABC, ABCMeta, abstractmethod
import asyncio
from collections.abc import Callable, Coroutine
from contextlib import asynccontextmanager
from logging import Logger
from typing import Any

from fastapi import FastAPI

from phederation.utils.logging import configure_logger

from .server import ActivityPubServer


class BackgroundTask(ABC):
    server: ActivityPubServer

    def __init__(
        self,
    ):
        self.initialized: bool = False

    async def initialize(self, server: ActivityPubServer):
        """Called once the app is initialized, and the server is ready."""
        self.server = server
        self.initialized = True
        self.logger: Logger = configure_logger(__name__, prefix=server.settings.federation.logging_prefix + "_background_task")

    async def on_startup(self):
        """Called once on startup of the app. This can be replaced by implementing classes."""
        self.logger.info(f"Starting: {type(self).__name__}.")

    async def on_shutdown(self):
        """Called once on shutdown of the app. This can be replaced by implementing classes."""
        self.logger.info(f"Stopping: {type(self).__name__}.")

    def cancel(self):
        """Cancel the task, e.g. if no more updates should be done."""
        raise asyncio.CancelledError()


class PeriodicBackgroundTask(BackgroundTask, metaclass=ABCMeta):
    def __init__(self, update_period: int):
        super(PeriodicBackgroundTask, self).__init__()
        self.update_period: int = update_period

        if update_period <= 0:
            raise ValueError(f"Update period in PeriodicBackgroundTask ({type(self)}) must be positive (is {update_period})")

    @abstractmethod
    async def on_update(self):
        """Called every update tick."""


async def periodic_task(task: PeriodicBackgroundTask):
    while True:
        try:
            await task.on_update()
        except asyncio.CancelledError:
            break
        except Exception as e:
            task.logger.error(f"Exception in task {type(task).__name__}: {e}.")
        await asyncio.sleep(task.update_period)


def between_callback(some_callback: Callable[..., Coroutine[None, None, Any]]):
    def run():
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

        loop.run_until_complete(some_callback())
        loop.close()

    return run


def create_lifespan(logger_prefix: str, background_tasks: list[BackgroundTask]):
    """Create the periodic background tasks for the instance.
    Note that this will NOT work during testing, as the asyncio loop is different there.

    Returns:
        lifespan object: The lifespan async context generator for FastAPI.lifespan
    """

    @asynccontextmanager
    async def lifespan(app: FastAPI):  # pyright: ignore[reportUnusedParameter]
        lifespan_logger = configure_logger(__name__ + "_lifespan", prefix=logger_prefix)
        tasks_created: list[asyncio.Task[None]] = []
        if len(background_tasks) == 0:
            lifespan_logger.info("No background tasks configured.")
            yield
        else:
            lifespan_logger.info("Starting periodic background tasks...")
            for task in background_tasks:
                threaded_task = asyncio.to_thread(between_callback(task.on_startup))
                tasks_created.append(asyncio.create_task(threaded_task))
                if isinstance(task, PeriodicBackgroundTask):
                    task_created = asyncio.create_task(periodic_task(task))
                    tasks_created.append(task_created)
            yield
            # Run on shutdown (if required)
            lifespan_logger.info("Shutting down periodic background tasks...")
            for task in tasks_created:
                _ = task.cancel()
            for task in background_tasks:
                await task.on_shutdown()

    return lifespan
