import json
import secrets
import string
from dataclasses import dataclass
from datetime import datetime, timezone
from hashlib import md5
from typing import cast

import redis


@dataclass
class RedisController:
    host: str
    port: int
    db: int
    password: str
    app_env: str

    def __post_init__(self) -> None:
        self.connection = redis.Redis(
            host=self.host, port=self.port, db=self.db, password=self.password
        )
        assert self.host, "REDIS_SERVER not properly populated"
        assert self.db >= 0, "REDIS_DB not properly populated"
        assert self.app_env, "APP_ENV not properly populated"

    # In order to pass jobs to CiviCRM, donate-neo must send it Resque-compatible messages.
    # CiviCRM runs resque-php workers which are constantly on the lookout for new jobs.
    # In order for them to properly receive and handle a job, we must:
    #   Update the "resque:queues" set with our own, named [APP_ENV]_web_donations, if none exists
    #   Add a new item to the list "resque:queue:[APP_ENV]_web_donations"
    def generate_resque_set_key(self) -> str:
        return self.app_env + "_web_donations"

    def generate_resque_list_key(self) -> str:
        return 'resque:queue:' + self.app_env + "_web_donations"

    # Resque generates an (insecure) ID for each queue item by retrieving the MD5 hash of a
    # (pseudo-)random, 23-character string of integers.
    def generate_id(self) -> str:
        return md5(
            ''.join(secrets.choice(string.digits) for i in range(23)).encode('utf-8')
        ).hexdigest()

    # Resque interprets "class" as the name of a method to execute, and "args" as, well, its args.
    # "id" and "queue_time" are used for Resqueue's internal queue management.
    # This implementation of Resque does not leverage the prefix.
    async def resque_send(self, message: str, args: dict) -> int:
        args_full = {
            "class": message,
            "args": args,
            "id": self.generate_id(),
            "prefix": "",
            "queue_time": datetime.now(timezone.utc).timestamp(),
        }
        # If the specified member exists in the set then SADD will ignore the command.
        # See: https://redis.io/docs/latest/commands/sadd/
        self.connection.sadd('resque:queues', self.generate_resque_set_key())
        # mypy complains if we don't explicitly cast the return to int
        return cast(int, self.connection.rpush(
            self.generate_resque_list_key(),
            json.dumps(args_full)
        ))
