import logging
import os

from fabric.api import env, settings, sudo
from fabric.contrib import files

from fabulaws.decorators import uses_fabric
from fabulaws.ubuntu.instances import UbuntuInstance
from fabulaws.ubuntu.packages.fail2ban import Fail2banMixin
from fabulaws.ubuntu.packages.memcached import MemcachedMixin
from fabulaws.ubuntu.packages.postgres import PostgresMixin
from fabulaws.ubuntu.packages.python import PythonMixin
from fabulaws.ubuntu.packages.rabbitmq import RabbitMqMixin
from fabulaws.ubuntu.packages.redis import RedisMixin

logger = logging.getLogger(__name__)

__all__ = [
    "CacheInstance",
    "DbPrimaryInstance",
    "DbReplicaInstance",
    "WorkerInstance",
    "WebInstance",
    "CombinedInstance",
]


class FirewallMixin(Fail2banMixin):
    pass


class BaseInstance(FirewallMixin, UbuntuInstance):
    deployment_dir = os.path.dirname(__file__)
    project_root = os.path.dirname(deployment_dir)

    def __init__(self, *args, **kwargs):
        for key in [
            "ami",
            "key_prefix",
            "admin_groups",
            "run_upgrade",
            "app_root",
            "deploy_user_home",
            "fs_type",
            "fs_encrypt",
            "ubuntu_mirror",
            "swap_multiplier",
            "instance_type",
            "deploy_user",
            "volume_size",
            "volume_type",
            "security_groups",
            "ebs_encrypt",
        ]:
            setattr(self, key, kwargs.pop(key, ""))
        if "terminate" not in kwargs:
            kwargs["terminate"] = False
        passwd = getattr(env, "luks_passphrase", None)
        self.volume_info = [
            ("/dev/sdf", self.app_root, self.volume_size, self.volume_type, passwd)
        ]
        self.default_swap_file = "%s/swapfile" % self.app_root
        super(BaseInstance, self).__init__(*args, **kwargs)
        self.app_dirs = ["/tmp", self.deploy_user_home]  # mixins add other dirs

    def _get_users(self):
        """
        Returns a list of tuples of (username, key_file_path).
        """
        users_dir = env.ssh_keys
        users = [
            (n, os.path.join(users_dir, n))
            for n in os.listdir(users_dir)
            if not n.endswith(".gecos")
        ]  # skip files ending in '.gecos'
        return users

    def _add_swap(self, path):
        sudo("mkswap -f {0}".format(path))
        with settings(warn_only=True):
            # sometimes mkswap seems to 'mount' the swap partition
            # automatically, so this command will fail
            sudo("swapon {0}".format(path))
        files.append("/etc/fstab", "{0} none swap sw 0 0".format(path), use_sudo=True)

    @uses_fabric
    def setup_swap(self):
        """Sets up swap partition"""
        swap_mb = self.server_memory * self.swap_multiplier
        devs = ["/dev/xvdb", "/dev/xvdc"]
        for dev in devs:
            print("attempting swap creation on {0}...".format(dev))
            if swap_mb <= 0:
                print("no additional swap needed; skipping")
                break
            if not files.exists(dev):
                print("no such device {0}; skipping".format(dev))
                continue
            size = sudo("blockdev --getsize64 {0}".format(dev))
            try:
                size = int(size) / 1024 / 1024
            except ValueError:
                print("no size found for {0}".format(dev))
                continue
            # decrement size regardless of whether or not we create swap; if
            # crypt device exists, assume we created it previously and don't
            # create even more swap
            swap_mb -= size
            with settings(warn_only=True):
                sudo("umount {0}".format(dev))
            crypt_name = "cryptswap-{0}".format(dev.split("/")[-1])
            if files.exists("/dev/mapper/{0}".format(crypt_name)):
                print("crypt device {0} already exists; skipping".format(crypt_name))
                continue
            sudo("cryptsetup -d /dev/urandom create {0} {1}".format(crypt_name, dev))
            files.append(
                "/etc/crypttab",
                "{0} {1} /dev/urandom swap".format(crypt_name, dev),
                use_sudo=True,
            )
            files.comment("/etc/fstab", dev, use_sudo=True)
            self._add_swap("/dev/mapper/{0}".format(crypt_name))
        if swap_mb > 0:
            if not files.exists(self.default_swap_file):
                sudo("fallocate -l {0}M {1}".format(swap_mb, self.default_swap_file))
                sudo("chown root:root {0}".format(self.default_swap_file))
                sudo("chmod 600 {0}".format(self.default_swap_file))
                self._add_swap(self.default_swap_file)
            else:
                print(
                    "swap file {0} already exists; skipping".format(
                        self.default_swap_file
                    )
                )
        # remove old swap at the end in case it's already in use (otherwise swapoff might fail)
        old_swap = ["/dev/xvda3"]
        for swap in old_swap:
            if files.contains("/proc/swaps", swap):
                sudo("swapoff {0}".format(swap))
                files.comment("/etc/fstab", swap, use_sudo=True)
                with settings(warn_only=True):
                    sudo("dd if=/dev/zero of={0} bs=1M".format(swap))

    @uses_fabric
    def setup_sudoers(self):
        """
        Creates the sudoers file on the server, based on the supplied template.
        """
        sudoers_file = os.path.join(self.deployment_dir, "templates", "sudoers")
        files.upload_template(
            sudoers_file, "/etc/sudoers.new", backup=False, use_sudo=True, mode=0o440
        )
        sudo("chown root:root /etc/sudoers.new")
        sudo("mv /etc/sudoers.new /etc/sudoers")

    def reset_authentication(self):
        """
        Delete's the 'ubuntu' user in the AMI once it's no longer needed.
        """
        user = self.user  # save user before it's overwritten by super() method
        super(BaseInstance, self).reset_authentication()
        if user == "ubuntu":
            with self:
                # disable the 'ubuntu' user so it can no longer log in
                sudo("usermod --expiredate 1 {}".format(user))

    @uses_fabric
    def create_deployer(self):
        """
        Creates a deployment user with a directory for Apache configurations.
        """
        user = self.deploy_user
        sudo("useradd -d {0} -m -s /bin/bash {1}".format(self.deploy_user_home, user))
        sudo("mkdir {0}/.ssh".format(self.deploy_user_home), user=user)

    @uses_fabric
    def update_deployer_keys(self):
        """
        Replaces deployer keys with the current sysadmin users keys.
        """
        user = self.deploy_user
        file_ = "{0}/.ssh/authorized_keys2".format(self.deploy_user_home)
        if files.exists(file_):
            sudo("rm {0}".format(file_), user=user)
        sudo("touch {0}".format(file_), user=user)
        for _, key_file in self._get_users():
            files.append(file_, open(key_file).read().strip(), use_sudo=True)

    def setup(self):
        """
        Creates sysadmin users and secures the required directories.
        """
        super(BaseInstance, self).setup()
        self.instance.modify_attribute(
            "blockDeviceMapping", ["%s=true" % v[0] for v in self.volume_info]
        )
        self.create_users(self._get_users())
        self.setup_sudoers()
        # needed for SSH agent forwarding during replication setup:
        self.reset_authentication()
        self.create_deployer()
        self.update_deployer_keys()
        self.bind_app_directories(self.app_dirs, self.app_root)
        self.setup_swap()  # after app (potentially secure) partition is created
        self.upgrade_packages()
        self.install_packages(["ntp"])  # keep date current


class SessionMixin(RedisMixin):
    """Mixin that creates a session store using Redis."""

    redis_bind = ""  # allow connections on all interfaces

    def __init__(self, *args, **kwargs):
        super(SessionMixin, self).__init__(*args, **kwargs)
        self.app_dirs.append("/var/lib/redis")


class CacheMixin(MemcachedMixin):
    """Mixin that creates a cache using Memcached."""

    memcached_bind = ""  # allow connections on all interfaces
    memcached_connections = 10000
    memcached_ulimit = 20000
    memcached_threads = 5

    def __init__(self, *args, **kwargs):
        super(CacheMixin, self).__init__(*args, **kwargs)

    @property
    @uses_fabric
    def memcached_memory(self):
        """Returns half of total server memory, in MB"""
        return self.server_memory / 2


class QueueMixin(RabbitMqMixin):
    """Mixin that creates a RabbitMQ user and host based on the Fabric env."""

    def __init__(self, *args, **kwargs):
        super(QueueMixin, self).__init__(*args, **kwargs)
        self.app_dirs.append("/var/lib/rabbitmq")

    def setup(self):
        """Create the RabbitMQ user and vhost."""

        super(QueueMixin, self).setup()
        self.create_mq_user(env.deploy_user, env.broker_password)
        self.create_mq_vhost(env.vhost)
        self.set_mq_vhost_permissions(env.vhost, env.deploy_user, '".*" ".*" ".*"')


class DbMixin(PostgresMixin):
    """Mixin that creates a database based on the Fabric env."""

    def __init__(self, *args, **kwargs):
        kwargs["db_settings"] = env.db_settings
        super(DbMixin, self).__init__(*args, **kwargs)
        self.app_dirs.append("/var/lib/postgresql")

    def setup(self):
        """Create the Postgres user and database based on the Fabric env."""

        super(DbMixin, self).setup()
        self.pg_allow_replication(
            "%s_repl" % env.database_user,
            env.database_password,
            self.postgresql_networks,
        )


class DbPrimaryMixin(DbMixin):
    """Mixin that creates a database based on the Fabric env."""

    def setup(self):
        """Create the Postgres user and database based on the Fabric env."""

        super(DbPrimaryMixin, self).setup()
        self.create_db_user(env.database_user, password=env.database_password)
        self.create_db(env.database_name, owner=env.database_user)


class DbReplicaMixin(DbMixin):
    """Mixin that creates a database based on the Fabric env."""

    def setup(self):
        """Create the Postgres user and database based on the Fabric env."""

        super(DbReplicaMixin, self).setup()
        if len(env.servers["db-primary"]) > 0:
            primary = env.servers["db-primary"][0]
            self.pg_copy_master(
                primary, "%s_repl" % env.database_user, env.database_password
            )


class AppMixin(PythonMixin):
    """
    Mixin that installs the Python application dependencies, including the appropriate
    versions of Python, PIP, and virtualenv.
    """

    @uses_fabric
    def install_less_and_yuglify(self):
        """
        Adds apt repo for node.js and NPM, installs NPM, and uses NPM to globally
        install the ``lessc`` binary used by django_compressor and pipeline
        and the ``yuglify`` binary used by pipeline.
        """

        node_version = getattr(env, "node_version", "6.x")
        sudo("curl -sL https://deb.nodesource.com/setup_%s | bash -" % node_version)
        sudo("export DEBIAN_FRONTEND=noninteractive ; apt-get -qq -y install nodejs")
        less_version = getattr(env, "less_version", "1.3.3")
        sudo("npm install -g less@%s" % less_version)
        sudo("npm install -g yuglify")

    @uses_fabric
    def install_system_packages(self):
        """Installs the system packages specified in the environment."""

        # Install required system packages for deployment, plus some extras
        packages = set(env.app_server_packages) | set(
            ["supervisor", "pgbouncer", "stunnel4"]
        )
        self.install_packages(packages)
        # supervisord doesn't start automatically on all Ubuntu versions,
        # so make sure it's started here
        sudo("service supervisor restart")

    @uses_fabric
    def create_webserver_user(self):
        """Create a user for gunicorn, celery, etc."""

        if env.webserver_user != env.deploy_user:  # deploy_user already exists
            sudo("useradd --system %(webserver_user)s" % env)

    def setup(self):
        """
        Creates necessary directories, installs required packages, and copies
        the required SSH keys to the server.
        """

        super(AppMixin, self).setup()
        self.install_less_and_yuglify()
        self.install_system_packages()
        self.create_webserver_user()


class WorkerMixin(AppMixin):
    """Mixin that creates a web application server."""

    swap_multiplier = 4  # create 4x the server's RAM in swap

    def __init__(self, *args, **kwargs):
        super(WorkerMixin, self).__init__(*args, **kwargs)


class WebMixin(AppMixin):
    """Mixin that creates a web application server."""

    swap_multiplier = 0  # no swap on the web servers (unhealthy servers will be replaced by Auto Scaling)

    def __init__(self, *args, **kwargs):
        super(WebMixin, self).__init__(*args, **kwargs)

    def setup(self):
        """Installs nginx."""

        super(WebMixin, self).setup()
        self.install_packages(["nginx"])


class CacheInstance(QueueMixin, CacheMixin, SessionMixin, BaseInstance):
    pass


class DbPrimaryInstance(DbPrimaryMixin, BaseInstance):
    pass


class DbReplicaInstance(DbReplicaMixin, BaseInstance):
    pass


class WorkerInstance(WorkerMixin, BaseInstance):
    pass


class WebInstance(WebMixin, BaseInstance):
    pass


class CombinedInstance(DbPrimaryMixin, WebMixin, BaseInstance):
    pass
