from enum import Enum
from django.conf import settings
from django.utils import timezone
from purist.models import AccountType, User
from middleware.common import get_woo_connection
from invitation.models import Invitation
import strictyaml


def get_available_bundles():

    with open(settings.WOO_BUNDLES_INFO_FILE, 'r') as stream:
        bundles = strictyaml.load(stream.read()).data
    return bundles


class SubscriptionStatus(Enum):
    PENDING = 'pending'
    ACTIVE = 'active'
    ON_HOLD = 'on-hold'
    EXPIRED = 'expired'
    CANCELLED = 'cancelled'
    PENDING_CANCEL = 'pending-cancel'
    PROCESSING = 'processing'


class BillingPeriod(Enum):
    MONTHLY = 'monthly'
    ANNUAL = 'annual'


class SubscriptionParseError(Exception):
    """Raised when the subscription being parsed contains not expected
    data.

    """
    pass


class SubscriptionError(Exception):
    pass


class SubscriptionManager:

    @classmethod
    def woo_get(cls, query):

        woo = get_woo_connection()
        return woo.get(query)

    @classmethod
    def create_subscription(cls, user, account_type=AccountType.BASIC,
                            billing_period=BillingPeriod.MONTHLY, paid=False,
                            next_payment_date=None, billing_email=None,
                            status=SubscriptionStatus.PENDING,
                            product_id=settings.WOO_PARENT_PROD_ID):
        """Creates a new WooCommerce order with a using account_type,
        billing_period, etc as specified by parameters. Default values
        if not specified will be:

        account_type = BASIC
        billing_period = monthly
        paid = False
        next_payment_date = 30 days from now
        billing_email = user.billing_email
        status = pending
        product_id = As configured in LDH

        Returns a Subscription object if the subscription was
        successfully created. Returns None if the subscription could
        no be created.

        """
        # Find variation_id for the account type and the billing
        # period
        bundles = get_available_bundles()
        variation_id = None
        for k, v in bundles.items():
            if (
                v['type'] == account_type.name and
                v['period'] == billing_period.value
            ):
                variation_id = k
                break

        if variation_id is None:
            # TODO: raise exception saying there is not bundle with
            # that account_type and billing_period
            return None

        user_wc_id = user.get_woocommerce_id()
        if user_wc_id is None:
            message = "Cannot create subscription for user {} with missing WooCommerceID.".format(user)
            raise Exception(message)

        if next_payment_date is None:
            next_payment_date = timezone.now() + timezone.timedelta(days=30)

        order_data = {
            "payment_method": "stripe",
            "payment_method_title": "Credit Card (Stripe)",
            "set_paid": paid,
            "line_items": [
                {
                    "product_id": product_id,
                    "variation_id": variation_id,
                    "quantity": 1,
                }
            ],
        }

        if billing_email:
            order_data['billing'] = {'email': billing_email}
        elif user.billing_email != '':
            order_data['billing'] = {'email': user.billing_email}

        order_data['customer_id'] = user_wc_id
        # TODO: Store the order in a Order class instance?
        order = user.woo_post_json('orders', order_data)

        if order is None:
            message = "Cannot create subscription for user {} because there was an error creating the associated order.".format(user)
            raise Exception(message)

        # Set billing period for subscription.
        bill_p = 'month'
        if billing_period == 'annual':
            bill_p = 'year'

        subs_data = {
            "line_items": [
                {
                    "product_id": product_id,
                    "variation_id": variation_id,
                    "quantity": 1,
                }
            ],
            "next_payment_date": next_payment_date.strftime(
                '%Y-%m-%d %H:%M:%S'),
            "billing_period": bill_p
        }

        subs_data['parent_id'] = order['id']
        subs_data['customer_id'] = order['customer_id']
        subs_data['status'] = status.value
        if billing_email:
            subs_data['billing'] = order_data['billing']

        sub = user.woo_post_json('subscriptions', subs_data)
        if sub is None:
            message = "Cannot create subscription for user {} because there was an error posting the subscription command.".format(user)
            raise Exception(message)

        new_sub = Subscription.parse_subscription(sub, user)
        # We save the order's order_key in the subscription for now
        # until we implement Order class. This is needed by BundleChange
        new_sub.order_key = order['order_key']
        return new_sub

    @classmethod
    def create_invitee_subscription(cls, user):
        try:
            invitation = Invitation.objects.get(guest=user)
        except Invitation.DoesNotExist:
            return None
        ren_date = invitation.owner.limit_set.first().renewal_date
        return cls.create_subscription(
            user, account_type=AccountType.INVITED,
            paid=True, next_payment_date=ren_date,
            status=SubscriptionStatus.ACTIVE
        )

    @classmethod
    def search_subscriptions(cls, user, status=None):
        result = []
        query = 'subscriptions?customer={}'.format(user.get_woocommerce_id())

        if status is not None:
            query += '&status={}'.format(status.value)

        r = cls.woo_get(query)

        if r.status_code == 200:
            q_subs = r.json()
            for q_sub in q_subs:
                try:
                    sub = Subscription.parse_subscription(q_sub, user)
                except SubscriptionParseError as e:
                    sub = None
                    # Should we use logger here instead of print?
                    print(repr(e))
                finally:
                    if sub:
                        result.append(sub)

        return result

    @classmethod
    def get_subscription(cls, sub_id):

        query = 'subscriptions/{}'.format(sub_id)
        r = cls.woo_get(query)

        if r.status_code == 200:
            q_sub = r.json()
            try:
                sub = Subscription.parse_subscription(q_sub)
            except SubscriptionParseError as e:
                sub = None
                print(repr(e))
            finally:
                return sub


class Subscription:

    @classmethod
    def woo_get(cls, query):

        woo = get_woo_connection()
        return woo.get(query)

    def __init__(
            self, user, subscription_id, product_id, variation_id,
            variation_name, next_payment_date, line_item_id, order_id,
            status=SubscriptionStatus.PENDING,
            account_type=AccountType.UNDEFINED,
            billing_period=BillingPeriod.MONTHLY, price=None, coupons=None
    ):
        self.user = user
        self.subscription_id = subscription_id
        self.product_id = product_id
        self.variation_id = variation_id
        self.variation_name = variation_name
        self.next_payment_date = next_payment_date
        self.status = status
        self.account_type = account_type
        self.billing_period = billing_period
        self.line_item_id = line_item_id
        self.order_id = order_id
        self.price = price
        self.coupons = coupons
        self.woo = None

    @classmethod
    def parse_subscription(cls, json_entry, user=None):

        subscription_id = str(json_entry["id"])

        # validation

        if len(json_entry["line_items"]) != 1:
            raise SubscriptionParseError(
                "Subscription {} does not have one line_items".format(
                    subscription_id)
            )

        line_item = json_entry["line_items"][0]

        quantity = line_item["quantity"]
        if quantity != 1:
            raise SubscriptionParseError(
                "Bad quantity {} in subscription {}".format(
                    quantity, subscription_id)
            )
        price = float(line_item.get('price', 0))
        coupons = json_entry['coupon_lines']
        line_item_id = line_item['id']

        # calculate next renewal date
        if json_entry["next_payment_date"] != "":
            next_renewal_naive = timezone.datetime.strptime(
                json_entry["next_payment_date"], "%Y-%m-%dT%H:%M:%S")
            next_renewal = timezone.make_aware(next_renewal_naive)
        else:
            next_renewal = None

        # get User
        if user is None:
            try:
                user_q = 'customers/' + str(json_entry['customer_id'])
                user_r = cls.woo_get(user_q).json()
                # remove the domain part of the WC username
                username = user_r['username'].split('@')[0]
                user = User.objects.get(username=username)
            except Exception:
                # TODO: handle more specific exceptions?
                # If we cannot find an LDH user the subscription is not
                # relevant
                return None

        # bundle_key value is different depending on the subscription
        # having variations or not
        if line_item.get('variation_id') != 0:
            bundle_key = line_item.get('variation_id')
        else:
            bundle_key = line_item.get('product_id')

        # get account_type and billing_period from configured bundles
        account_type = AccountType.UNDEFINED
        billing_period = BillingPeriod.MONTHLY
        bundles = get_available_bundles()
        sub_bundle = bundles.get(str(bundle_key), None)

        if sub_bundle:
            sub_acc_type = sub_bundle.get('type', None)
            if 'GROUP' == sub_acc_type:
                account_type = AccountType.GROUP
            elif 'COMPLETE' == sub_acc_type:
                account_type = AccountType.COMPLETE
            elif 'BASIC' == sub_acc_type:
                account_type = AccountType.BASIC
            elif 'INVITED' == sub_acc_type:
                account_type = AccountType.INVITED

            sub_period = sub_bundle.get('period', None)
            try:
                billing_period = BillingPeriod(sub_period)
            except ValueError:
                billing_period = BillingPeriod.MONTHLY

        # create subscription
        return cls(
            user=user,
            subscription_id=subscription_id,
            product_id=line_item.get('product_id'),
            variation_id=line_item.get('variation_id'),
            variation_name=line_item.get('name'),
            next_payment_date=next_renewal,
            status=SubscriptionStatus(json_entry['status']),
            account_type=account_type,
            billing_period=billing_period,
            line_item_id=line_item_id,
            order_id=json_entry['parent_id'],
            price=price, coupons=coupons
        )

    def upgrade_choices(self, add_downgrade=False):

        choices = []
        bundles = get_available_bundles()
        cur_bundle = bundles.get(str(self.variation_id), None)
        if cur_bundle is None:
            return choices
        for upgrade in cur_bundle['upgrades']:
            choices.append((upgrade, bundles[upgrade]['title']))
        if add_downgrade:
            for downgrade in cur_bundle['downgrades']:
                choices.append((downgrade, bundles[downgrade]['title']))

        return choices

    def save(self):
        if self.woo is None:
            self.woo = get_woo_connection()

        if self.billing_period == BillingPeriod.ANNUAL:
            b_period = 'year'
        elif self.billing_period == BillingPeriod.MONTHLY:
            b_period = 'month'
        else:
            b_period = ''
        if self.next_payment_date is None:
            next_p_date = ''
        else:
            next_p_date = self.next_payment_date.strftime('%Y-%m-%d %H:%M:%S')
        data = {
            'id': self.subscription_id,
            'status': self.status.value,
            'customer_id': self.user.get_woocommerce_id(),
            'line_items': [
                {
                    'id': self.line_item_id,
                    'name': self.variation_name,
                    'product_id': self.product_id,
                    'variation_id': self.variation_id,
                }
            ],
            'next_payment_date': next_p_date,
        }

        if self.user.billing_email == '':
            b_email = self.user.email
        else:
            b_email = self.user.billing_email
        if b_email != '':
            data['billing'] = {'email': b_email}

        if b_period != '':
            data['billing_period'] = b_period
            data['billing_interval'] = '1'

        query = 'subscriptions/{}'.format(self.subscription_id)
        req = self.woo.put(query, data)
        if req.status_code != 200:
            try:
                raise SubscriptionError('Error when saving the subscription: '
                                        + req.json()['message'])
            except ValueError:
                raise SubscriptionError('Error when saving the subscription')

    @property
    def is_active(self):
        if self.status == SubscriptionStatus.ACTIVE:
            return True
        return False

    def apply_coupon(self, code):
        if self.woo is None:
            self.woo = get_woo_connection()

        # Get all coupons
        req = self.woo.get('coupons')
        if req.status_code != 200:
            try:
                raise SubscriptionError('Error when retrieving coupons: '
                                        + req.json()['message'])
            except ValueError:
                raise SubscriptionError('Error when retrieving coupons')

        # Find the coupon by code an get useful data
        discount_type, amount, data = None, None, None
        for coupon in req.json():
            coupon_code = coupon.get('code', None)
            if coupon_code == code:
                discount_type = coupon.get('discount_type', None)
                amount = coupon.get('amount', None)
                break

        # Check if coupon was already applied
        for coupon in self.coupons:
            if coupon.get('code') == code:
                # coupon already applied
                return None
        if discount_type == 'recurring_percent' and amount:
            # Calculate total price with discount
            total = round(self.price * (1 - float(amount) * 0.01), 2)

            # Prepare data to update subscription
            line_items = [
                {
                    'id': self.line_item_id,
                    'name': self.variation_name,
                    'product_id': self.product_id,
                    'variation_id': self.variation_id,
                    'total': str(total)
                }
            ]
            data = {
                'coupon_lines': [
                    {
                        'code': code,
                        'discount': str(round(self.price - total, 2))
                    }
                ],
                'line_items': line_items
            }
            query = 'subscriptions/{}'.format(self.subscription_id)
            req = self.woo.put(query, data)
            if req.status_code != 200:
                try:
                    raise SubscriptionError('Error when applying coupon to subscription: '
                                            + req.json()['message'])
                except ValueError:
                    raise SubscriptionError('Error when applying coupon to subscription.')
