from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING, Generic, TypeVar, final

from typing_extensions import override

from jobify._internal.common.constants import EMPTY, INFINITY, JobStatus
from jobify._internal.exceptions import JobFailedError, JobNotCompletedError

if TYPE_CHECKING:
    from collections.abc import Callable, Generator
    from datetime import datetime

    from jobify._internal.configuration import Cron
    from jobify._internal.cron_parser import CronParser
    from jobify._internal.storage.base import Storage

ReturnT = TypeVar("ReturnT")


@final
class CronContext(Generic[ReturnT]):
    __slots__: tuple[str, ...] = (
        "cron",
        "cron_parser",
        "failure_count",
        "job",
        "offset",
        "run_count",
    )

    def __init__(  # noqa: PLR0913
        self,
        *,
        cron: Cron,
        cron_parser: CronParser,
        failure_count: int = 0,
        job: Job[ReturnT],
        offset: datetime,
        run_count: int,
    ) -> None:
        self.cron = cron
        self.cron_parser = cron_parser
        self.failure_count = failure_count
        self.job = job
        self.offset = offset
        self.run_count = run_count

    def is_run_exceeded_by_limit(self) -> bool:
        if self.cron.max_runs == INFINITY:
            return False
        return self.run_count >= self.cron.max_runs

    def is_failure_allowed_by_limit(self) -> bool:
        return self.failure_count < self.cron.max_failures


@final
class Job(Generic[ReturnT]):
    __slots__: tuple[str, ...] = (
        "_cron_context",
        "_event",
        "_handle",
        "_result",
        "_storage",
        "_unregister_hook",
        "exception",
        "exec_at",
        "id",
        "status",
    )

    def __init__(
        self,
        *,
        job_id: str,
        storage: Storage,
        exec_at: datetime,
        unregister_hook: Callable[[str], None],
        job_status: JobStatus = JobStatus.PENDING,
    ) -> None:
        self._unregister_hook = unregister_hook
        self._event = asyncio.Event()
        self._result: ReturnT = EMPTY
        self._storage = storage
        self._handle: asyncio.Handle | None = None
        self._cron_context: CronContext[ReturnT] | None = None
        self.id = job_id
        self.status = job_status
        self.exception: Exception | None = None
        self.exec_at = exec_at

        self._event.set()

    @property
    def cron_expression(self) -> str | None:
        if self._cron_context is not None:
            return self._cron_context.cron.expression
        return None

    @override
    def __repr__(self) -> str:
        if self.cron_expression is not None:
            cron_info = f", cron={self.cron_expression!r}"
        else:
            cron_info = ""
        return (
            f"<{type(self).__name__} "
            f"id={self.id!r}, "
            f"status={self.status.value!r}, "
            f"exec_at={self.exec_at.isoformat()!r}"
            f"{cron_info}>"
        )

    def __await__(self) -> Generator[object, None, ReturnT]:
        async def _await() -> ReturnT:
            await self.wait()
            return self.result()

        return _await().__await__()

    def bind_handle(self, handle: asyncio.Handle) -> None:
        self._handle = handle

    def bind_cron_context(self, ctx: CronContext[ReturnT]) -> None:
        self._cron_context = ctx

    def result(self) -> ReturnT:
        if self.status is JobStatus.SUCCESS or self._result is not EMPTY:
            return self._result
        if self.status is JobStatus.FAILED:
            raise JobFailedError(
                self.id,
                reason=str(self.exception),
            ) from self.exception
        raise JobNotCompletedError

    def set_result(self, val: ReturnT, *, status: JobStatus) -> None:
        self._result = val
        self.status = status

    def set_exception(self, exc: Exception, *, status: JobStatus) -> None:
        self.exception = exc
        self.status = status

    def update(self, *, exec_at: datetime, status: JobStatus) -> None:
        self._event = asyncio.Event()
        self.status = status
        self.exec_at = exec_at

    def is_done(self) -> bool:
        return self._event.is_set()

    def is_cron(self) -> bool:
        return self._cron_context is not None

    def is_reschedulable(self) -> bool:
        return self.status not in (
            JobStatus.PERMANENTLY_FAILED,
            JobStatus.CANCELLED,
        )

    async def wait(self) -> None:
        """Wait until the job is done.

        If the job is already completed, this method returns immediately.
        Safe for concurrent use by multiple coroutines.
        """
        _ = await self._event.wait()

    async def cancel(self) -> None:
        self.status = JobStatus.CANCELLED
        self._cancel()
        await self._storage.delete_schedule(self.id)

    def _cancel(self) -> None:
        self._event.set()
        self._unregister_hook(self.id)
        if self._handle is not None:
            self._handle.cancel()
