# By: Riasat Ullah
# This file contains functions for handling coupons.

from dbqueries import db_billings
from exceptions.user_exceptions import InvalidCoupon
from taskcallrest import settings
from utils import errors, file_storage, helpers, key_manager, s3, times, var_names
from validations import string_validator


def get_issued_coupons():
    '''
    Get all the coupons that have been issued from a s3 file.
    :return: (dict) -> {coupon: details, ...}
    '''
    coupons = s3.read_json(file_storage.S3_BUCKET_TASKCALL_PROD_DATA, file_storage.S3_KEY_COUPON_ISSUED)
    return coupons


def update_issued_coupons(upd_coupons):
    '''
    Update the issued coupons file.
    :param upd_coupons: (dict) updated coupons
    '''
    s3.update_json(file_storage.S3_BUCKET_TASKCALL_PROD_DATA, file_storage.S3_KEY_COUPON_ISSUED, upd_coupons)


def update_coupon_usage_count(coupon, increment=1, new_expiry=None):
    '''
    Update the current usage field of a coupon. This will be used to update the number of times the coupon has been
    redeemed and expire those that can only be used by a single account globally.
    :param coupon: coupon code
    :param increment: how much to increase the count by
    :param new_expiry: (datetime.datetime) new expiry time; can be used to expire the coupon after redemption
    '''
    issued_coupons = get_issued_coupons()
    if coupon in issued_coupons:
        if var_names.total_redemptions not in issued_coupons[coupon]:
            issued_coupons[coupon][var_names.total_redemptions] = 0
        issued_coupons[coupon][var_names.total_redemptions] += increment

        if new_expiry is not None:
            issued_coupons[coupon][var_names.expires_on] = new_expiry
    update_issued_coupons(issued_coupons)


def make_coupons(conn, timestamp, expiry_date, credit_type, quantity, account_id=None, email=None, upload=False):
    '''
    Make new coupons. They can be directly uploaded to the global issued coupons repository on s3.
    :param conn: db connection
    :param timestamp: timestamp the coupons should be stamped as having been created on
    :param expiry_date: last timestamp the coupon can be redeemed by (not the same as credit validity)
    :param credit_type: type of credit that will be applied upon coupon redemption
    :param quantity: number of coupons that should be minted
    :param account_id: the ID of the account the coupon is for
    :param email: email address the coupon should be sent to (it will restrict redemption to this email only)
    :param upload: True if the coupons should be uploaded to the global repo after minting
    :return: (list of dict) -> [{coupon: details, ...}]
    '''
    credit_type_details = db_billings.get_credit_types(conn, timestamp, credit_type=credit_type)
    if len(credit_type_details) == 0:
        raise LookupError(errors.err_internal_credit_type_unknown + ' - ' + credit_type)

    credit_type_details = credit_type_details[credit_type]
    coup_length = credit_type_details[var_names.coupon_length]
    coup_det = {var_names.created_on: helpers.jsonify_unserializable(timestamp),
                var_names.expires_on: helpers.jsonify_unserializable(expiry_date),
                var_names.credit_type: credit_type,
                var_names.total_redemptions: 0}

    if account_id is not None:
        assert isinstance(account_id, str)
        coup_det[var_names.account_id] = account_id

    if email is not None:
        assert string_validator.is_email_address(email)
        coup_det[var_names.email] = email

    issued_coupons = get_issued_coupons()
    new_coupons = dict()
    for i in range(0, quantity):
        exists = True
        while exists:
            minted_coupon = key_manager.generate_alphanumeric_key(coup_length, capitalized=True)
            if minted_coupon not in issued_coupons:
                exists = False
                new_coupons[minted_coupon] = coup_det

    if upload:
        issued_coupons.update(new_coupons)
        update_issued_coupons(issued_coupons)

    return new_coupons


def redeem_coupon(conn, timestamp, coupon, org_id):
    '''
    Redeems a coupon for an organization. First checks for validity of the coupon with respect to itself and
    the organization that is trying to redeem it.
    :param conn: db connection
    :param timestamp: timestamp when this request is being made
    :param coupon: coupon code
    :param org_id: ID of the organization
    :errors: AssertionError, DatabaseError, InvalidCoupon, LookupError
    '''
    issued_coupons = get_issued_coupons()
    org_coupons = db_billings.get_organization_credits(conn, timestamp, org_id)
    if coupon in issued_coupons:
        issued_details = issued_coupons[coupon]
        coupon_expiry = times.get_timestamp_from_string(issued_details[var_names.expires_on])
        coupon_credit_type = issued_details[var_names.credit_type]
        coupon_total_redemptions = issued_details[var_names.total_redemptions]

        for_account = issued_details[var_names.account_id] if var_names.account_id in issued_details else None
        for_email = issued_details[var_names.email] if var_names.email in issued_details else None

        org_same_credit_type_count = sum([1 for item in org_coupons
                                          if item[var_names.credit_type] == coupon_credit_type])

        credit_type_details = db_billings.get_credit_types(conn, timestamp, credit_type=coupon_credit_type)
        if len(credit_type_details) == 0:
            raise LookupError(errors.err_internal_credit_type_unknown)
        ctd = credit_type_details[coupon_credit_type]
        req_subscription_id = ctd[var_names.subscription_id]
        validity_after_reg = ctd[var_names.validity_after_registration]

        # check if the coupon can be redeemed
        if timestamp > coupon_expiry or org_same_credit_type_count >= ctd[var_names.max_organization_redemption] or\
            coupon_total_redemptions >= ctd[var_names.max_global_redemption] or\
                (ctd[var_names.host_region] is not None and settings.REGION != ctd[var_names.host_region]):
            raise InvalidCoupon

        if validity_after_reg is not None or for_account is not None or for_email is not None or\
                req_subscription_id is not None:
            rdm_info = db_billings.get_organization_redemption_info(conn, timestamp, org_id)
            days_since_registration = (timestamp - rdm_info[var_names.registration_date]).days

            if (validity_after_reg is not None and days_since_registration > validity_after_reg) or\
                (for_account is not None and for_account != rdm_info[var_names.account_id]) or\
                (for_email is not None and for_email != rdm_info[var_names.email]) or \
                    (req_subscription_id is not None and req_subscription_id != rdm_info[var_names.subscription_id]):
                raise InvalidCoupon

        db_billings.add_credit(conn, timestamp, ctd[var_names.valid_for], org_id, coupon_credit_type,
                               ctd[var_names.credit_currency], float(ctd[var_names.credit_amount]),
                               coupon, issued_details)

        new_expiry = None
        if coupon_total_redemptions + 1 == ctd[var_names.max_global_redemption]:
            new_expiry = helpers.jsonify_unserializable(timestamp)
        update_coupon_usage_count(coupon, new_expiry=new_expiry)
    else:
        raise InvalidCoupon
