from typing import (
    Optional,
    Sequence,
)

from redis import Redis
from redis.client import Pipeline
from service_locator_api.shared.exception import RegistrationNotFound
from ._Registration import Registration
from ._RegistrationRepository import RegistrationRepository


class RedisRegistrationRepository(RegistrationRepository):
    registration_key_template = "serviceLocator:registration:{reg_id}"
    service_name_key_template = "serviceLocator:serviceName:{service_name}"
    all_services_key = "serviceLocator:allServices"

    def __init__(
        self,
        host: str,
        port: int,
        db: int,
        expiration: float
    ):
        self.client = Redis(
            host=host,
            port=port,
            db=db,
            decode_responses=True,
        )
        self.expiration = int(expiration * 1000)

    def add_registration(self, registration: Registration):
        registration_key = self.registration_key_template.format(reg_id=registration.reg_id)
        service_name_key = self.service_name_key_template.format(service_name=registration.service_name)

        def add_registration_in_transaction(pipeline: Pipeline):
            pipeline.multi()
            pipeline.set(
                name=registration_key,
                value=str(registration),
                px=self.expiration,
            )
            pipeline.rpush(
                service_name_key,
                f"{registration.service_address}\0{registration.reg_id}",
            )
            pipeline.sadd(
                self.all_services_key,
                registration.service_name,
            )
            pipeline.execute()

        self.client.transaction(
            func=add_registration_in_transaction,
            watches=[self.all_services_key, registration_key, service_name_key],
        )

    def lookup(self, service_name: str) -> Optional[str]:
        service_name_key = self.service_name_key_template.format(service_name=service_name)

        def get_current_address_in_transaction(pipeline: Pipeline):
            index = 0
            ret_address = None
            while True:
                left_value = self.client.lindex(
                    name=service_name_key,
                    index=index
                )
                if left_value is None:
                    break

                current_address, reg_id = left_value.split("\0")

                registration_key = self.registration_key_template.format(reg_id=reg_id)
                pipeline.watch(registration_key)

                registration_data = pipeline.get(name=registration_key)
                if registration_data is None:
                    index += 1
                    continue

                ret_address = current_address
                break

            pipeline.multi()
            pipeline.lpop(
                name=service_name_key,
                count=index + 1,
            )
            if left_value is not None:
                pipeline.rpush(
                    service_name_key,
                    left_value,
                )
            pipeline.execute()

            return ret_address

        return self.client.transaction(
            func=get_current_address_in_transaction,
            watches=[service_name_key],
            value_from_callable=True,
        )

    def keep_alive(self, reg_id: str):
        response = self.client.expire(
            name=self.registration_key_template.format(reg_id=reg_id),
            time=self.expiration,
        )
        if response == 0:
            raise RegistrationNotFound()

    def clean_registrations(self):
        pass

    def get_all_registrations(self) -> Sequence[Registration]:
        return []

    def get_registered_services(self) -> Sequence[str]:
        return []

    def close(self):
        self.client.close()
