from datetime import timedelta
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.utils import timezone

from ..models import (
    Account,
    Article,
    ArticleCategory,
    Checkout,
    CheckoutStatement,
    Inventory,
    InventoryArticle,
    Operation,
    OperationGroup,
)

User = get_user_model()


def _create_user_and_account(user_attrs, account_attrs, perms=None):
    """
    Create a user and its account, and assign permissions to this user.

    Arguments
        user_attrs (dict): User data (first name, last name, password...).
        account_attrs (dict): Account data (department, kfet password...).
        perms (list of str: 'app.perm'): These permissions will be assigned to
            the created user. No permission are assigned by default.

    If 'password' is not given in 'user_attrs', username is used as password.

    If 'kfet.is_team' is in 'perms' and 'password' is not in 'account_attrs',
    the account password is 'kfetpwd_<user pwd>'.

    """
    user_pwd = user_attrs.pop("password", user_attrs["username"])
    user = User.objects.create(**user_attrs)
    user.set_password(user_pwd)
    user.save()

    account_attrs["cofprofile"] = user.profile
    kfet_pwd = account_attrs.pop("password", "kfetpwd_{}".format(user_pwd))

    account = Account.objects.create(**account_attrs)

    if perms is not None:
        user = user_add_perms(user, perms)

        if "kfet.is_team" in perms:
            account.change_pwd(kfet_pwd)
            account.save()

    return user


def create_user(username="user", trigramme="000", **kwargs):
    """
    Create a user without any permission and its kfet account.

    username and trigramme are accepted as arguments (defaults to 'user' and
    '000').

    user_attrs, account_attrs and perms can be given as keyword arguments to
    customize the user and its kfet account.

    # Default values

    User
        * username: user
        * password: user
        * first_name: first
        * last_name: last
        * email: mail@user.net
    Account
        * trigramme: 000

    """
    user_attrs = kwargs.setdefault("user_attrs", {})

    user_attrs.setdefault("username", username)
    user_attrs.setdefault("first_name", "first")
    user_attrs.setdefault("last_name", "last")
    user_attrs.setdefault("email", "mail@user.net")

    account_attrs = kwargs.setdefault("account_attrs", {})
    account_attrs.setdefault("trigramme", trigramme)

    return _create_user_and_account(**kwargs)


def create_team(username="team", trigramme="100", **kwargs):
    """
    Create a user, member of the kfet team, and its kfet account.

    username and trigramme are accepted as arguments (defaults to 'team' and
    '100').

    user_attrs, account_attrs and perms can be given as keyword arguments to
    customize the user and its kfet account.

    # Default values

    User
        * username: team
        * password: team
        * first_name: team
        * last_name: member
        * email: mail@team.net
    Account
        * trigramme: 100
        * kfet password: kfetpwd_team

    """
    user_attrs = kwargs.setdefault("user_attrs", {})

    user_attrs.setdefault("username", username)
    user_attrs.setdefault("first_name", "team")
    user_attrs.setdefault("last_name", "member")
    user_attrs.setdefault("email", "mail@team.net")

    account_attrs = kwargs.setdefault("account_attrs", {})
    account_attrs.setdefault("trigramme", trigramme)

    perms = kwargs.setdefault("perms", [])
    perms.append("kfet.is_team")

    return _create_user_and_account(**kwargs)


def create_root(username="root", trigramme="200", **kwargs):
    """
    Create a superuser and its kfet account.

    username and trigramme are accepted as arguments (defaults to 'root' and
    '200').

    user_attrs, account_attrs and perms can be given as keyword arguments to
    customize the user and its kfet account.

    # Default values

    User
        * username: root
        * password: root
        * first_name: super
        * last_name: user
        * email: mail@root.net
        * is_staff, is_superuser: True
    Account
        * trigramme: 200
        * kfet password: kfetpwd_root

    """
    user_attrs = kwargs.setdefault("user_attrs", {})

    user_attrs.setdefault("username", username)
    user_attrs.setdefault("first_name", "super")
    user_attrs.setdefault("last_name", "user")
    user_attrs.setdefault("email", "mail@root.net")
    user_attrs["is_superuser"] = user_attrs["is_staff"] = True

    account_attrs = kwargs.setdefault("account_attrs", {})
    account_attrs.setdefault("trigramme", trigramme)

    return _create_user_and_account(**kwargs)


def get_perms(*labels):
    """Return Permission instances from a list of '<app>.<perm_codename>'."""
    perms = {}
    for label in set(labels):
        app_label, codename = label.split(".", 1)
        perms[label] = Permission.objects.get(
            content_type__app_label=app_label, codename=codename
        )
    return perms


def user_add_perms(user, perms_labels):
    """
    Add perms to a user.

    Args:
        user (User instance)
        perms (list of str 'app.perm_name')

    Returns:
        The same user (refetched from DB to avoid missing perms)

    """
    perms = get_perms(*perms_labels)
    user.user_permissions.add(*perms.values())

    # If permissions have already been fetched for this user, we need to reload
    # it to avoid using of the previous permissions cache.
    # https://docs.djangoproject.com/en/dev/topics/auth/default/#permission-caching
    return User.objects.get(pk=user.pk)


def create_checkout(**kwargs):
    """
    Factory to create a checkout.
    See defaults for unpassed arguments in code below.
    """
    if "created_by" not in kwargs or "created_by_id" not in kwargs:
        try:
            team_account = Account.objects.get(cofprofile__user__username="team")
        except Account.DoesNotExist:
            team_account = create_team().profile.account_kfet
        kwargs["created_by"] = team_account
    kwargs.setdefault("name", "Checkout")
    kwargs.setdefault("valid_from", timezone.now() - timedelta(days=14))
    kwargs.setdefault("valid_to", timezone.now() - timedelta(days=14))

    return Checkout.objects.create(**kwargs)


def create_operation_group(content=None, **kwargs):
    """
    Factory to create an OperationGroup and a set of related Operation.

    It aims to get objects for testing purposes with minimal setup, and
    preserving consistency.
    For this, it uses, and creates if necessary, default objects for unpassed
    arguments.

    Args:
        content: list of dict
            Describe set of Operation to create along the OperationGroup.
            Each item is passed to the Operation factory.
        kwargs:
            Used to control OperationGroup creation.

    """
    if content is None:
        content = []

    # Prepare OperationGroup creation.

    # Set 'checkout' for OperationGroup if unpassed.
    if "checkout" not in kwargs and "checkout_id" not in kwargs:
        try:
            checkout = Checkout.objects.get(name="Checkout")
        except Checkout.DoesNotExist:
            checkout = create_checkout()
        kwargs["checkout"] = checkout

    # Set 'on_acc' for OperationGroup if unpassed.
    if "on_acc" not in kwargs and "on_acc_id" not in kwargs:
        try:
            on_acc = Account.objects.get(cofprofile__user__username="user")
        except Account.DoesNotExist:
            on_acc = create_user().profile.account_kfet
        kwargs["on_acc"] = on_acc

    # Set 'is_cof' for OperationGroup if unpassed.
    if "is_cof" not in kwargs:
        # Use current is_cof status of 'on_acc'.
        kwargs["is_cof"] = kwargs["on_acc"].cofprofile.is_cof

    # Create OperationGroup.
    group = OperationGroup.objects.create(**kwargs)

    # We can now create objects referencing this OperationGroup.

    # Process set of related Operation.
    if content:
        # Create them.
        operation_list = []
        for operation_kwargs in content:
            operation = create_operation(group=group, **operation_kwargs)
            operation_list.append(operation)

        # Update OperationGroup accordingly, for consistency.
        for operation in operation_list:
            if not operation.canceled_at:
                group.amount += operation.amount
        group.save()

    return group


def create_operation(**kwargs):
    """
    Factory to create an Operation for testing purposes.

    If you give a 'group' (OperationGroup), it won't update it, you have do
    this "manually". Prefer using OperationGroup factory to get a consistent
    group with operations.

    """
    if "group" not in kwargs and "group_id" not in kwargs:
        # To get a consistent OperationGroup (amount...) for the operation
        # in-creation, prefer using create_operation_group factory with
        # 'content'.
        kwargs["group"] = create_operation_group()

    if "type" not in kwargs:
        raise RuntimeError("Can't create an Operation without 'type'.")

    # Apply defaults for purchase
    if kwargs["type"] == Operation.PURCHASE:
        if "article" not in kwargs:
            raise NotImplementedError(
                "One could write a create_article factory. Right now, you must"
                "pass an 'article'."
            )

        # Unpassed 'article_nb' defaults to 1.
        kwargs.setdefault("article_nb", 1)

        # Unpassed 'amount' will use current article price and quantity.
        if "amount" not in kwargs:
            if "addcost_for" in kwargs or "addcost_amount" in kwargs:
                raise NotImplementedError(
                    "One could handle the case where 'amount' is missing and "
                    "addcost applies. Right now, please pass an 'amount'."
                )
            kwargs["amount"] = -kwargs["article"].price * kwargs["article_nb"]

    return Operation.objects.create(**kwargs)


def create_checkout_statement(**kwargs):
    if "checkout" not in kwargs:
        kwargs["checkout"] = create_checkout()
    if "by" not in kwargs:
        try:
            team_account = Account.objects.get(cofprofile__user__username="team")
        except Account.DoesNotExist:
            team_account = create_team().profile.account_kfet
        kwargs["by"] = team_account
    kwargs.setdefault("balance_new", kwargs["checkout"].balance)
    kwargs.setdefault("balance_old", kwargs["checkout"].balance)
    kwargs.setdefault("amount_taken", Decimal(0))

    return CheckoutStatement.objects.create(**kwargs)


def create_article(**kwargs):
    kwargs.setdefault("name", "Article")
    kwargs.setdefault("price", Decimal("2.50"))
    kwargs.setdefault("stock", 20)
    if "category" not in kwargs:
        kwargs["category"] = create_article_category()

    return Article.objects.create(**kwargs)


def create_article_category(**kwargs):
    kwargs.setdefault("name", "Category")
    return ArticleCategory.objects.create(**kwargs)


def create_inventory(**kwargs):
    if "by" not in kwargs:
        try:
            team_account = Account.objects.get(cofprofile__user__username="team")
        except Account.DoesNotExist:
            team_account = create_team().profile.account_kfet
        kwargs["by"] = team_account

    return Inventory.objects.create(**kwargs)


def create_inventory_article(**kwargs):
    if "inventory" not in kwargs:
        kwargs["inventory"] = create_inventory()
    if "article" not in kwargs:
        kwargs["article"] = create_article()
    kwargs.setdefault("stock_old", kwargs["article"].stock)
    kwargs.setdefault("stock_new", kwargs["article"].stock)

    return InventoryArticle.objects.create(**kwargs)