import logging

from choicesenum import ChoicesEnum
from choicesenum.django.fields import EnumIntegerField
from cryptography.fernet import Fernet
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as BaseUserManager
from django.core import validators
from django.db import models
from django.utils.crypto import salted_hmac
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _
from middleware.common import get_woo_connection

from ldapregister.models import LdapPerson, LdapGroup
from limitmonitor import models as limitmonitor_models
from limitmonitor.tunnel import TunnelManager

log = logging.getLogger(__name__)


class AccountType(ChoicesEnum):
    UNDEFINED = 0, 'Undefined'
    BASIC = 1, 'Basic'
    COMPLETE = 2, 'Complete'
    INVITED = 3, 'Invited'
    GROUP = 4, 'Family pack'

    @classmethod
    def from_str(cls, value):
        """Convert a string to AccountType returning AccountType.UNDEFINED if
        the string is not one of the AccountType names.
        """
        for choice in cls.choices():
            if choice[0].name == value.upper():
                return choice[0]
        return cls.UNDEFINED


@deconstructible
class UsernameValidator(validators.RegexValidator):
    regex = r'^[A-Za-z][A-Za-z0-9]*$'
    message = _(
        'Enter a valid address prefix. Must start with a letter, followed by letters or numbers.'
        ' No punctuation or special characters.'
    )


class UserManager(BaseUserManager):

    def create_user(self, username, email=None, password=None, **extra_fields):
        """Create regular users in LDAP, with no Django password."""

        return super(UserManager, self).create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email=None, password=None, **extra_fields):
        """Create superusers with a Django password."""

        return super(UserManager, self).create_superuser(username, email, password, **extra_fields)


class User(AbstractUser):
    objects = UserManager()
    REQUIRED_FIELDS = []
    username_validator = UsernameValidator()
    woo_connection = None

    username = models.CharField(
        _('username'),
        max_length=150,
        unique=True,
        help_text=_('Start with a letter, followed by letters or numbers.'),
        validators=[username_validator],
        error_messages={
            'unique': _("An account with that address already exists."),
        },
    )
    email = models.EmailField(_('email address'), blank=False, unique=False)  # recovery email address
    tunnel_user = models.CharField(max_length=250, default=None, null=True)
    tunnel_password = models.CharField(max_length=250, default=None, null=True)
    account_type = EnumIntegerField(enum=AccountType,
                                    default=AccountType.UNDEFINED)
    billing_email = models.EmailField(default='', null=False)
    purge = models.IntegerField(default=None, null=True)
    wcid = models.IntegerField(default=None, null=True)

    def clean(self):
        super().clean()
        self.email = self.email.lower()

    @classmethod
    def normalize_username(cls, username):
        username = super(User, cls).normalize_username(username)

        username = username.lower()
        suffix = "@" + settings.SITE_DOMAIN.lower()
        offset = 0 - len(suffix)

        if username.endswith(suffix):
            username = username[:offset]

        return username

    def get_ldap(self):
        return LdapPerson.objects.get(uid=self.get_username())

    def has_ldap(self):
        result = LdapPerson.objects.filter(uid=self.get_username())
        return len(result) == 1

    def create_ldap(self):
        username = self.get_username()
        mail = self.get_identity()
        LdapPerson.objects.create(uid=username, cn=username, sn=username, mail=mail)

    def set_ldap_password(self, raw_password):
        ldap_person = self.get_ldap()
        ldap_person.change_password(raw_password)

    def set_ldap_group(self, group_id):
        ldap_person = self.get_ldap()
        try:
            ldap_group = LdapGroup.objects.get(cn=group_id)
        except LdapGroup.DoesNotExist:
            logging.exception('LDAP group {} does not exist.'.format(group_id))
            return
        u_dn = ldap_person.build_dn()
        if u_dn not in ldap_group.members:
            ldap_group.members.append(u_dn)
            ldap_group.save()

    def remove_ldap_group(self, group_id):
        ldap_person = self.get_ldap()
        try:
            ldap_group = LdapGroup.objects.get(cn=group_id)
        except LdapGroup.DoesNotExist:
            logging.exception('LDAP group {} does not exist.'.format(group_id))
            return
        u_dn = ldap_person.build_dn()
        if u_dn in ldap_group.members:
            ldap_group.members.remove(ldap_person.build_dn())
            ldap_group.save()

    def woo_get_json(self, query):

        try:
            if self.woo_connection is None:
                woo = get_woo_connection()
            result = woo.get(query).json()
        except Exception as e:
            logging.exception("Could not retrieve WooCommerce ID with query " + query)
            result = None

        return result

    def woo_post_json(self, query, data):

        try:
            if self.woo_connection is None:
                woo = get_woo_connection()
            result = woo.post(query, data).json()
        except Exception as e:
            logging.exception("Could not post " + query + " with data " + str(data))
            result = None

        return result

    def woo_put_json(self, query, data):

        try:
            if self.woo_connection is None:
                woo = get_woo_connection()
            result = woo.put(query, data).json()
        except Exception as e:
            logging.exception("Could not put " + query + " with data " + str(data))
            result = None

        return result

    def get_woocommerce_id(self):

        if self.wcid is not None:
            return self.wcid
        else:
            query = "customers?role=all&limit=1&email=" + self.get_identity()
            json = self.woo_get_json(query)

            if json is not None and len(json) == 1:
                self.wcid = json[0]["id"]
                return self.wcid

        return None

    def create_woocommerce_account(self):

        query = "customers"

        data = {
            "email": self.get_identity(),
            "first_name": self.username,
            "last_name": "@" + settings.SITE_DOMAIN,
            "username": self.get_identity(),
            "password": make_password(None),  # unusable password
            "billing": {"email": self.email}
        }

        result = self.woo_post_json(query, data)
        if result:
            self.wcid = result.get("id", None)
        return result

    def delete_woocommerce_account(self):

        woo_id = self.get_woocommerce_id()
        query = "customers/{}?force=true".format(woo_id)

        try:
            if self.woo_connection is None:
                woo = get_woo_connection()
            result = woo.delete(query).json()
        except Exception as e:
            logging.exception("Could not delete WooCommerce account: " + query)
            result = None

        return result

    def set_woocommerce_password(self, raw_password, woocommerce_id=None):

        if woocommerce_id is None:
            woocommerce_id = self.get_woocommerce_id()

        query = "customers/" + str(woocommerce_id)

        data = {
            "password": raw_password,
        }

        return self.woo_put_json(query, data)

    def get_identity(self):
        return self.get_username() + "@" + settings.SITE_DOMAIN.lower()

    def get_woocommerce_billing_email(self):

        query = "customers?role=all&email=" + self.get_identity()
        json = self.woo_get_json(query)

        if json is not None and len(json) == 1:
            billing_email = json[0]["billing"]["email"]
            if billing_email != '':
                return billing_email

        return None

    def set_woocommerce_billing_email(self, woocommerce_id=None):

        data = {
            "billing": {
                "email": self.billing_email
            },
        }

        if woocommerce_id is None:
            woocommerce_id = self.get_woocommerce_id()
        query = "customers/" + str(woocommerce_id)

        return self.woo_put_json(query, data)

    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):

        # save django user
        super(User, self).save(force_insert, force_update, using, update_fields)

        # create LDAP user (if required)
        if not self.has_ldap():
            self.create_ldap()

        # force null Django password (will use LDAP password instead)
        self.set_unusable_password()

        # create WooCommerce account (if required)
        if self.get_woocommerce_id() is None:
            self.create_woocommerce_account()

        # create any missing limits
        limitmonitor_models.create_missing_user_limits(self)

    def set_password(self, raw_password):

        # force null Django password (will use LDAP password)
        self.set_unusable_password()

        # create LDAP user (if required)
        if self.get_username():

            if not self.has_ldap():
                self.create_ldap()

            # set LDAP password
            self.set_ldap_password(raw_password)

            # FIXME: we actually need to update WooCommerce LDAP authentication
            if self.get_woocommerce_id() is None:
                self.create_woocommerce_account()

            self.set_woocommerce_password(raw_password)

    def get_session_auth_hash(self):
        """
        Return an HMAC of the password field.
        """
        key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash"
        return salted_hmac(key_salt, self.get_username()).hexdigest()  # FIXME: should use LDAP password value!

    def has_tunnel_account(self):
        """Returns True if the user has a tunnel account. Returns False if the
        user does NOT have a tunnel account.
        """
        return self.tunnel_user is not None

    def generate_tunnel_account(self):
        """Creates a new tunnel account if the user does NOT have a tunnel
        account. Returns True when the account is created and False
        when it couldn't be created.
        """
        if not self.has_tunnel_account():
            tun = TunnelManager(settings.TUNNEL_IDENTITY,
                                settings.TUNNEL_SECRET,
                                settings.TUNNEL_HOST)
            new_account = tun.create_account()
            if new_account:
                tun_user = encrypt(new_account['username'])
                tun_passwd = encrypt(new_account['password'])
                self.tunnel_user = tun_user
                self.tunnel_password = tun_passwd
                self.save()
                return True
            return False
        else:
            return True

    def pause_tunnel_account(self):
        """Pauses the tunnel account for the user, setting it is_active to
        False. Returns True when it was possible to pause the account
        and False when it couldn't.
        """
        if self.has_tunnel_account():
            tun = TunnelManager(settings.TUNNEL_IDENTITY,
                                settings.TUNNEL_SECRET,
                                settings.TUNNEL_HOST)
            tun_user = decrypt(self.tunnel_user)
            result = tun.pause_account(tun_user)
            if result:
                tunnel_limit = self.limit_set.filter(
                    service=settings.LM_SERVICES.TUNNEL).first()
                tunnel_limit.is_active = False
                tunnel_limit.save()
                return True
            return False

    def resume_tunnel_account(self):
        """Resumes the tunnel account for the user, setting it is_active to
        True. Returns True when it was possible to resume the account
        and False when it was not.
        """
        if self.has_tunnel_account():
            tun = TunnelManager(settings.TUNNEL_IDENTITY,
                                settings.TUNNEL_SECRET,
                                settings.TUNNEL_HOST)
            tun_user = decrypt(self.tunnel_user)
            result = tun.resume_account(tun_user)
            if result:
                tunnel_limit = self.limit_set.filter(
                    service=settings.LM_SERVICES.TUNNEL).first()
                tunnel_limit.is_active = True
                tunnel_limit.save()
                return True
            return False

    def terminate_tunnel_account(self):
        """Terminates the tunnel account for the user. Returns True when it
        was possible to terminate the account and False when it was
        not.
        """
        if self.has_tunnel_account():
            tun = TunnelManager(settings.TUNNEL_IDENTITY,
                                settings.TUNNEL_SECRET,
                                settings.TUNNEL_HOST)
            tun_user = decrypt(self.tunnel_user)
            result = tun.terminate_account(tun_user)
            if result:
                self.tunnel_user = None
                self.tunnel_password = None
                self.save()
                tunnel_limit = self.limit_set.filter(
                    service=settings.LM_SERVICES.TUNNEL).first()
                tunnel_limit.is_active = False
                tunnel_limit.save()
                return True
            return False

    def get_tunnel_credentials(self):
        """Returns tunnel account credentials for the user decrypting
        them. Returns the tuple (tun_user, tun_passwd) if the user has
        a tunnel account and is activated. Returns None in any other
        case.
        """
        if self.has_tunnel_account():
            tunnel_limit = self.limit_set.filter(
                    service=settings.LM_SERVICES.TUNNEL).first()
            if tunnel_limit.is_active:
                tun_user = decrypt(self.tunnel_user)
                tun_passwd = decrypt(self.tunnel_password)
                return (tun_user, tun_passwd)
        return None

    @property
    def contact_email(self):
        if self.account_type is AccountType.COMPLETE:
            return self.get_identity()
        else:
            return self.email

    def get_enabled_limits(self):
        limits = self.limit_set.all()
        result = []
        for l in limits:
            if l.is_enabled():
                result.append(l.service)
        return result

    def get_woocommerce_orders(self):
        user_wc_id = self.get_woocommerce_id()
        if user_wc_id is None:
            # The user does not have WC account
            return None
        orders = self.woo_get_json(
            'orders?customer={}'.format(user_wc_id))

        if orders is not None and len(orders) >= 1:
            return orders
        else:
            return None

    def get_woocommerce_subscriptions(self, status=None):
        user_wc_id = self.get_woocommerce_id()
        if user_wc_id is None:
            # The user does not have WC account
            return None
        if status:
            query = 'subscriptions?customer={}&status={}'.format(
                user_wc_id, status)
        else:
            query = 'subscriptions?customer={}'.format(user_wc_id)
        subscriptions = self.woo_get_json(query)

        if subscriptions is not None and len(subscriptions) >= 1:
            return subscriptions
        else:
            return None

    def remove_woocommerce_pii(self):
        """Removes all personally identifying information stored in WC.
        """
        user_wc_id = self.get_woocommerce_id()
        if user_wc_id is None:
            # The user does not have WC account
            return None
        # remove user data
        clean_shipping_data = {
            "first_name": "",
            "last_name": "",
            "company": "",
            "address_1": "",
            "address_2": "",
            "city": "",
            "state": "",
            "postcode": "",
            "country": ""
        }
        clean_billing_data = {
            "first_name": "",
            "last_name": "",
            "company": "",
            "address_1": "",
            "address_2": "",
            "city": "",
            "state": "",
            "postcode": "",
            "country": "",
            "email": self.get_identity(),
            "phone": ""
        }
        user_data = {
            "email": self.get_identity(),
            "first_name": "",
            "last_name": "",
            "username": self.get_identity(),
            "billing": clean_billing_data,
            "shipping": clean_shipping_data,
        }
        query = "customers/{}".format(user_wc_id)
        self.woo_put_json(query, user_data)

        # Remove orders PII
        clean_data = {
            "billing": clean_billing_data,
            "shipping": clean_shipping_data,
        }
        orders = self.get_woocommerce_orders()
        if orders:
            for order in orders:
                order_id = order.get('id', None)
                if order_id:
                    query_o = "orders/{}".format(order_id)
                    self.woo_post_json(query_o, clean_data)

        # Remove subscription PII
        subs = self.woo_get_json('subscriptions?customer={}'.format(
            user_wc_id))
        if len(subs) >= 1:
            for sub in subs:
                sub_id = sub.get('id', None)
                if sub_id:
                    query_s = "subscriptions/{}".format(sub_id)
                    self.woo_post_json(query_s, clean_data)

    def get_invitees(self):
        result = []
        invitations = self.invitation_set.filter(consumed=True).filter(
            guest__is_active=True)
        for inv in invitations:
            result.append(inv.guest)
        return result

    def can_change_bundle(self):
        if self.account_type == AccountType.INVITED:
            return False
        else:
            return len(
                self.get_upgrade_choices(add_downgrade=True)
            ) > 0


def encrypt(data):
    # Encrypt data with Fernet
    if data is None:
        ret = None
    else:
        fer = Fernet(settings.TUNNEL_KEY)
        ret = fer.encrypt(data.encode())
    return ret


def decrypt(data):
    # Decrypt data with Fernet
    if data is None:
        ret = None
    else:
        fer = Fernet(settings.TUNNEL_KEY)
        data = data.encode()
        ret = fer.decrypt(data).decode()
    return ret
