import ipaddress
import warnings
from CTFd.cache import cache
from CTFd.utils import get_config
from flask_redis import FlaskRedis
from redis.exceptions import LockError

from .db import DBContainer


class CacheProvider:
    def __init__(self, app, *args, **kwargs):
        if app.config['CACHE_TYPE'] == 'redis':
            self.provider = RedisCacheProvider(app, *args, **kwargs)
        elif app.config['CACHE_TYPE'] in ['filesystem', 'simple']:
            if not hasattr(CacheProvider, 'cache'):
                CacheProvider.cache = {}
            self.provider = FilesystemCacheProvider(app, *args, **kwargs)
            self.init_port_sets()

    def init_port_sets(self):
        self.clear()

        containers = DBContainer.get_all_container()
        used_port_list = []
        for container in containers:
            if container.port != 0:
                used_port_list.append(container.port)
        for port in range(int(get_config("whale:frp_direct_port_minimum", 29000)),
                          int(get_config("whale:frp_direct_port_maximum", 28000)) + 1):
            if port not in used_port_list:
                self.add_available_port(port)

        from .docker import get_docker_client
        client = get_docker_client()

        docker_subnet = get_config("whale:docker_subnet", "174.1.0.0/16")
        docker_subnet_new_prefix = int(
            get_config("whale:docker_subnet_new_prefix", "24"))

        exist_networks = []
        available_networks = []

        for network in client.networks.list(filters={'label': 'prefix'}):
            exist_networks.append(str(network.attrs['Labels']['prefix']))

        for network in list(ipaddress.ip_network(docker_subnet).subnets(new_prefix=docker_subnet_new_prefix)):
            if str(network) not in exist_networks:
                available_networks.append(str(network))

        self.add_available_network_range(*set(available_networks))

    def __getattr__(self, name):
        return self.provider.__getattribute__(name)


class FilesystemCacheProvider:
    def __init__(self, app, *args, **kwargs):
        warnings.warn(
            '\n[CTFd Whale] Warning: looks like you are using filesystem cache. '
            '\nThis is for TESTING purposes only, DO NOT USE on production sites.',
            RuntimeWarning
        )
        self.key = 'ctfd_whale_lock-' + str(kwargs.get('user_id', 0))
        self.global_port_key = "ctfd_whale-port-set"
        self.global_network_key = "ctfd_whale-network-set"

    def clear(self):
        cache.set(self.global_port_key, set())
        cache.set(self.global_network_key, set())

    def add_available_network_range(self, *ranges):
        s = cache.get(self.global_network_key)
        s.update(ranges)
        cache.set(self.global_network_key, s)

    def get_available_network_range(self):
        try:
            s = cache.get(self.global_network_key)
            r = s.pop()
            cache.set(self.global_network_key, s)
            return r
        except KeyError:
            return None

    def add_available_port(self, port):
        s = cache.get(self.global_port_key)
        s.add(port)
        cache.set(self.global_port_key, s)

    def get_available_port(self):
        try:
            s = cache.get(self.global_port_key)
            r = s.pop()
            cache.set(self.global_port_key, s)
            return r
        except KeyError:
            return None

    def acquire_lock(self):
        # for testing purposes only, so no need to set this limit
        return True

    def release_lock(self):
        return True


class RedisCacheProvider(FlaskRedis):
    def __init__(self, app, *args, **kwargs):
        super().__init__(app)
        self.key = 'ctfd_whale_lock-' + str(kwargs.get('user_id', 0))
        self.current_lock = None
        self.global_port_key = "ctfd_whale-port-set"
        self.global_network_key = "ctfd_whale-network-set"

    def clear(self):
        self.delete(self.global_port_key)
        self.delete(self.global_network_key)

    def add_available_network_range(self, *ranges):
        self.sadd(self.global_network_key, *ranges)

    def get_available_network_range(self):
        return self.spop(self.global_network_key).decode()

    def add_available_port(self, port):
        self.sadd(self.global_port_key, str(port))

    def get_available_port(self):
        return int(self.spop(self.global_port_key))

    def acquire_lock(self):
        lock = self.lock(name=self.key, timeout=10)

        if not lock.acquire(blocking=True, blocking_timeout=2.0):
            return False

        self.current_lock = lock
        return True

    def release_lock(self):
        if self.current_lock is None:
            return False

        try:
            self.current_lock.release()

            return True
        except LockError:
            return False
