from typing import Any, cast

import pglock
from django.db.models import QuerySet
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.utils.timezone import now
from dramatiq.broker import Broker
from structlog.stdlib import get_logger

from django_dramatiq_postgres.conf import Conf
from django_dramatiq_postgres.models import ScheduleBase, TaskState


class Scheduler:
    broker: Broker
    db_alias: str

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.logger = get_logger(__name__, type(self))

    @cached_property
    def model(self) -> type[ScheduleBase]:
        schedule_model = cast(str, Conf().schedule_model)
        model: type[ScheduleBase] = import_string(schedule_model)
        return model

    @property
    def query_set(self) -> QuerySet[ScheduleBase]:
        return self.model._default_manager.using(self.db_alias).filter(paused=False)

    def process_schedule(self, schedule: ScheduleBase) -> None:
        schedule.next_run = schedule.compute_next_run()
        schedule.send(self.broker)
        schedule.save(update_fields=["next_run"])

    def _lock(self) -> pglock.advisory:
        return pglock.advisory(
            lock_id=f"{Conf().channel_prefix}.scheduler",
            side_effect=pglock.Return,
            timeout=0,
        )

    def _run(self) -> int:
        count = 0
        for schedule in self.query_set.filter(next_run__lt=now()):
            if (
                schedule.tasks.using(self.db_alias)  # type: ignore[attr-defined]
                .exclude(state__in=(TaskState.DONE, TaskState.REJECTED))
                .exists()
            ):
                self.logger.debug("Skipping schedule, tasks already exists", schedule=schedule)
                continue
            self.process_schedule(schedule)
            count += 1
        return count

    def run(self) -> int:
        with self._lock() as lock_acquired:
            if not lock_acquired:
                self.logger.debug("Could not acquire lock, skipping scheduling")
                return -1
            count = self._run()
            self.logger.info("Sent scheduled tasks", count=count)
        return count
