from typing import Any, cast

import redis.asyncio as redis

from asyncmq.stores.base import BaseJobStore


class RedisJobStore(BaseJobStore):
    """
    A concrete implementation of `BaseJobStore` using Redis as the backend
    for storing and retrieving job data.

    Job data is stored as JSON strings in Redis keys, and the IDs of jobs
    belonging to a queue are tracked in a Redis Set.
    """

    def __init__(self, redis_url: str | None = None, *, redis_client: redis.Redis | None = None) -> None:
        """
        Initializes the RedisJobStore by establishing a connection to Redis.

        Args:
            redis_url: The connection URL for the Redis instance. If None,
                       a client must be injected via `redis_client`.
            redis_client: An existing Redis client instance for custom usage.
        """
        if redis_client is not None:
            self.redis: redis.Redis = redis_client
        elif redis_url:
            self.redis: redis.Redis = redis.from_url(redis_url, decode_responses=True)  # type: ignore
        else:
            self.redis = None  # to be set externally

    def _key(self, queue_name: str, job_id: str) -> str:
        """
        Generates the Redis key string used to store the data for a specific job.

        The key format is typically `jobs:{queue_name}:{job_id}`.

        Args:
            queue_name: The name of the queue the job belongs to.
            job_id: The unique identifier of the job.

        Returns:
            The formatted Redis key string.
        """
        # Return the formatted Redis key for a job.
        return f"jobs:{queue_name}:{job_id}"

    def _set_key(self, queue_name: str) -> str:
        """
        Generates the Redis key string used for the Set that stores the IDs
        of all jobs belonging to a specific queue.

        The key format is typically `jobs:{queue_name}:ids`.

        Args:
            queue_name: The name of the queue.

        Returns:
            The formatted Redis key string for the Set.
        """
        # Return the formatted Redis key for the set of job IDs for a queue.
        return f"jobs:{queue_name}:ids"

    async def save(self, queue_name: str, job_id: str, data: dict[str, Any]) -> None:
        """
        Asynchronously saves the data for a specific job in Redis.

        The job data dictionary is serialized to a JSON string and stored
        in a Redis key generated by `_key`. The job ID is also added to
        the Redis Set tracked by `_set_key` for the given queue.

        Args:
            queue_name: The name of the queue the job belongs to.
            job_id: The unique identifier of the job.
            data: The dictionary containing the job's data to be saved.
        """
        # Serialize the job data dictionary to a JSON string.
        job_data_json: str = self._json_serializer.to_json(data)
        # Store the JSON data in Redis using the job's key.
        await self.redis.set(self._key(queue_name, job_id), job_data_json)
        # Add the job ID to the set of job IDs for this queue.
        await self.redis.sadd(self._set_key(queue_name), job_id)

    async def load(self, queue_name: str, job_id: str) -> dict[str, Any] | None:
        """
        Asynchronously loads the data for a specific job from Redis by its ID.

        Retrieves the JSON string stored at the job's key, parses it into
        a dictionary, and returns it.

        Args:
            queue_name: The name of the queue the job belongs to.
            job_id: The unique identifier of the job.

        Returns:
            A dictionary containing the job's data if the key exists and
            contains valid JSON, otherwise None.
        """
        # Retrieve the raw JSON string from Redis using the job's key.
        raw: str | None = await self.redis.get(self._key(queue_name, job_id))
        # Parse the JSON string into a dictionary if it exists, otherwise return None.
        return cast(dict[str, Any], self._json_serializer.to_dict(raw)) if raw else None

    async def delete(self, queue_name: str, job_id: str) -> None:
        """
        Asynchronously deletes the data for a specific job from Redis.

        Removes the Redis key storing the job data and removes the job ID
        from the Set of job IDs for the queue.

        Args:
            queue_name: The name of the queue the job belongs to.
            job_id: The unique identifier of the job.
        """
        # Delete the Redis key storing the job data.
        await self.redis.delete(self._key(queue_name, job_id))
        # Remove the job ID from the set of job IDs for this queue.
        await self.redis.srem(self._set_key(queue_name), job_id)

    async def all_jobs(self, queue_name: str) -> list[dict[str, Any]]:
        """
        Asynchronously retrieves data for all jobs associated with a specific
        queue by fetching all IDs from the queue's Set and loading each job
        individually.

        Args:
            queue_name: The name of the queue.

        Returns:
            A list of dictionaries, where each dictionary is the data for a job
            in the specified queue. Jobs that cannot be loaded (e.g., key deleted
            between fetching ID and loading) are skipped.
        """
        # Get all job IDs from the set for the given queue.
        ids: list[str] = await self.redis.smembers(self._set_key(queue_name))
        jobs_data: list[dict[str, Any]] = [
            # Load each job by its ID and add it to the list if loading is successful.
            job
            for job_id in ids
            if job_id and (job := await self.load(queue_name, job_id)) is not None
        ]
        # Return the list of loaded job data dictionaries.
        return jobs_data

    async def jobs_by_status(self, queue_name: str, status: str) -> list[dict[str, Any]]:
        """
        Asynchronously retrieves data for jobs in a specific queue that are
        currently in a given status by loading all jobs and filtering in memory.

        Note: This implementation is inefficient for large queues as it loads
        all jobs first. A more scalable backend would use Redis sorted sets
        or other structures to track jobs by status natively.

        Args:
            queue_name: The name of the queue.
            status: The status of the jobs to retrieve (e.g., "waiting", "active").

        Returns:
            A list of dictionaries, where each dictionary contains the data
            for a job matching the criteria.
        """
        # Load all jobs for the specified queue.
        all_jobs: list[dict[str, Any]] = await self.all_jobs(queue_name)
        # Filter the list of jobs to include only those matching the specified status.
        filtered_jobs: list[dict[str, Any]] = [job for job in all_jobs if job and job.get("status") == status]
        # Return the filtered list of jobs.
        return filtered_jobs
