import logging

from datetime import datetime
from uuid import uuid4

from flask import Flask, request
from flask_restful import Resource, Api

from config import etcd_client as client, config as config
from stripe_utils import StripeUtils
from schemas import (
    make_return_message, ValidationException, UserRegisterPaymentSchema,
    AddProductSchema, ProductOrderSchema, OrderListSchema, create_schema
)
from helper import get_plan_id_from_product, calculate_charges


class ListProducts(Resource):
    @staticmethod
    def get():
        products = client.get_prefix('/v1/products/')
        products = [
            product
            for product in [p.value for p in products]
            if product['active']
        ]
        prod_dict = {}
        for p in products:
            prod_dict[p['usable-id']] = {
                'name': p['name'],
                'description': p['description'],
            }
        logger.debug('Products = {}'.format(prod_dict))
        return prod_dict, 200


class AddProduct(Resource):
    @staticmethod
    def post():
        data = request.get_json(silent=True) or {}

        try:
            logger.debug('Got data: {}'.format(str(data)))
            validator = AddProductSchema(data)
            validator.is_valid()
        except ValidationException as err:
            return make_return_message(err, 400)
        else:
            cleaned_values = validator.get_cleaned_values()
            previous_product = cleaned_values.get('product', None)
            if previous_product:
                if not cleaned_values['update']:
                    return make_return_message('Product already exists. Pass --update to update the product.')
                else:
                    product_uuid = previous_product.pop('uuid')
            else:
                product_uuid = uuid4().hex

            product_value = cleaned_values['specs']

            product_key = '/v1/products/{}'.format(product_uuid)
            product_value['uuid'] = product_uuid

            logger.debug('Adding product data: {}'.format(str(product_value)))
            client.put(product_key, product_value)
            if not previous_product:
                return make_return_message('Product created.')
            else:
                return make_return_message('Product updated.')


class UserRegisterPayment(Resource):
    @staticmethod
    def post():
        data = request.get_json(silent=True) or {}

        try:
            logger.debug('Got data: {}'.format(str(data)))
            validator = UserRegisterPaymentSchema(data)
            validator.is_valid()
        except ValidationException as err:
            return make_return_message(err, 400)
        else:
            cleaned_values = validator.get_cleaned_values()
            last4 = data['card_number'].strip()[-4:]

            stripe_utils = StripeUtils()

            # Does customer already exist ?
            stripe_customer = stripe_utils.get_stripe_customer_from_email(cleaned_values['user']['mail'])

            # Does customer already exist ?
            if stripe_customer is not None:
                logger.debug('Customer {}-{} exists already'.format(
                    cleaned_values['username'], cleaned_values['user']['mail'])
                )

                # Check if the card already exists
                ce_response = stripe_utils.card_exists(
                        stripe_customer.id, cc_number=data['card_number'],
                        exp_month=int(data['expiry_month']),
                        exp_year=int(data['expiry_year']),
                        cvc=data['cvc'])

                if ce_response['response_object']:
                    message = 'The given card ending in {} exists already.'.format(last4)
                    return make_return_message(message, 400)

                elif ce_response['response_object'] is False:
                    # Associate card with user
                    logger.debug('Adding card ending in {}'.format(last4))
                    token_response = stripe_utils.get_token_from_card(
                        data['card_number'], data['cvc'], data['expiry_month'],
                        data['expiry_year']
                    )
                    if token_response['response_object']:
                        logger.debug('Token {}'.format(token_response['response_object'].id))
                        resp = stripe_utils.associate_customer_card(
                            stripe_customer.id, token_response['response_object'].id
                        )
                        if resp['response_object']:
                            return make_return_message(
                                'Card ending in {} registered as your payment source'.format(last4)
                            )
                    else:
                        return make_return_message('Error with payment gateway. Contact support', 400)
                else:
                    return make_return_message('Error: {}'.format(ce_response['error']), 400)
            else:
                # Stripe customer does not exist, create a new one
                logger.debug(
                    'Customer {} does not exist, creating new'.format(cleaned_values['user']['mail'])
                )
                token_response = stripe_utils.get_token_from_card(
                    cleaned_values['card_number'], cleaned_values['cvc'],
                    cleaned_values['expiry_month'], cleaned_values['expiry_year']
                )
                if token_response['response_object']:
                    logger.debug('Token {}'.format(token_response['response_object'].id))

                    # Create stripe customer
                    stripe_customer_resp = stripe_utils.create_customer(
                        name=cleaned_values['card_holder_name'],
                        token=token_response['response_object'].id,
                        email=cleaned_values['user']['mail'],
                        address=cleaned_values['address']
                    )
                    stripe_customer = stripe_customer_resp['response_object']

                    if stripe_customer:
                        logger.debug('Created stripe customer {}'.format(stripe_customer.id))
                        return make_return_message(
                            'Card ending in {} registered as your payment source'.format(last4)
                        )
                    else:
                        return make_return_message('Error with card. Contact support', 400)
                else:
                    return make_return_message('Error with payment gateway. Contact support', 400)


class ProductOrder(Resource):
    @staticmethod
    def post():
        data = request.get_json(silent=True) or {}

        try:
            validator = ProductOrderSchema(data)
            validator.is_valid()
        except ValidationException as err:
            return make_return_message(err, 400)
        else:
            cleaned_values = validator.get_cleaned_values()
            stripe_utils = StripeUtils()

            product = cleaned_values['product']

            # Check the user has a payment source added
            stripe_customer = stripe_utils.get_stripe_customer_from_email(cleaned_values['user']['mail'])

            if not stripe_customer or len(stripe_customer.sources) == 0:
                return make_return_message('Please register your payment method first.', 400)

            try:
                product_schema = create_schema(product, data)
                product_schema = product_schema()
                product_schema.is_valid()
            except ValidationException as err:
                return make_return_message(err, 400)
            else:
                transformed_data = product_schema.get_cleaned_values()
                logger.debug('Tranformed data: {}'.format(transformed_data))
                one_time_charge, recurring_charge = calculate_charges(product, transformed_data)
                recurring_charge = int(recurring_charge)

                if not cleaned_values['pay']:
                    return make_return_message(
                        'You would be charged {} CHF one time and {} CHF every {}. '
                        'Add --pay to command to order.'.format(
                            one_time_charge, recurring_charge, product['recurring_period']
                        )
                    )

                with client.client.lock('product-order') as _:
                    # Initiate a one-time/subscription based on product type
                    if recurring_charge > 0:
                        logger.debug('Product {} is recurring payment'.format(product['name']))
                        plan_id = get_plan_id_from_product(product)
                        res = stripe_utils.get_or_create_stripe_plan(
                            product_name=product['name'],
                            stripe_plan_id=plan_id, amount=recurring_charge,
                            interval=product['recurring_period'],
                        )
                        if res['response_object']:
                            logger.debug('Obtained plan {}'.format(plan_id))
                            subscription_res = stripe_utils.subscribe_customer_to_plan(
                                stripe_customer.id,
                                [{'plan': plan_id}]
                            )
                            subscription_obj = subscription_res['response_object']
                            if subscription_obj is None or subscription_obj.status != 'active':
                                return make_return_message(
                                    'Error subscribing to plan. Detail: {}'.format(subscription_res['error']), 400
                                )
                            else:
                                order_obj = {
                                    'order-id': uuid4().hex,
                                    'ordered-at': datetime.now().isoformat(),
                                    'product': product['usable-id'],
                                    'one-time-price': one_time_charge,
                                    'recurring-price': recurring_charge,
                                    'recurring-period': product['recurring_period']
                                }
                                client.put(
                                    '/v1/user/{}/orders/{}'.format(
                                        cleaned_values['username'], order_obj['order-id']
                                    ), order_obj
                                )
                                product['quantity'] -= 1
                                client.put('/v1/products/{}'.format(product['uuid']), product)

                                return {
                                    'message': 'Order Successful.',
                                    **order_obj
                                }
                        else:
                            logger.error('Could not create plan {}'.format(plan_id))
                            return make_return_message('Something wrong happened. Contact administrator', 400)

                    elif recurring_charge == 0 and one_time_charge > 0:
                        logger.debug('Product {} is one-time payment'.format(product['name']))
                        charge_response = stripe_utils.make_charge(
                            amount=one_time_charge,
                            customer=stripe_customer.id
                        )
                        stripe_onetime_charge = charge_response.get('response_object')

                        # Check if the payment was approved
                        if not stripe_onetime_charge:
                            msg = charge_response.get('error')
                            return make_return_message('Error subscribing to plan. Details: {}'.format(msg), 400)

                        order_obj = {
                            'order-id': uuid4().hex,
                            'ordered-at': datetime.now().isoformat(),
                            'product': product['usable-id'],
                            'one-time-price': one_time_charge,
                        }
                        client.put(
                            '/v1/user/{}/orders/{}'.format(cleaned_values['username'], order_obj['order-id']),
                            order_obj
                        )
                        product['quantity'] -= 1
                        client.put('/v1/products/{}'.format(product['uuid']), product)

                        return {'message': 'Order successful', **order_obj}, 200


class OrderList(Resource):
    @staticmethod
    def post():
        data = request.get_json(silent=True) or {}

        try:
            validator = OrderListSchema(data)
            validator.is_valid()
        except ValidationException as err:
            return make_return_message(err, 400)
        else:
            cleaned_values = validator.get_cleaned_values()
            orders = client.get_prefix('/v1/user/{}/orders'.format(cleaned_values['username']))
            orders_dict = {
                order.value['order-id']: {
                    **order.value
                }
                for order in orders
            }
            logger.debug('Orders = {}'.format(orders_dict))
            return {'orders': orders_dict}, 200


if __name__ == '__main__':
    logger = logging.getLogger()
    logger.setLevel(logging.DEBUG)
    log_formater = logging.Formatter('[%(filename)s:%(lineno)d] %(message)s')

    stream_logger = logging.StreamHandler()
    stream_logger.setFormatter(log_formater)

    # file_logger = logging.FileHandler('log.txt')
    # file_logger.setLevel(logging.DEBUG)
    # file_logger.setFormatter(log_formater)

    logger.addHandler(stream_logger)
    # logger.addHandler(file_logger)

    app = Flask(__name__)

    api = Api(app)
    api.add_resource(ListProducts, '/product/list')
    api.add_resource(AddProduct, '/product/add')
    api.add_resource(ProductOrder, '/product/order')
    api.add_resource(UserRegisterPayment, '/user/register_payment')
    api.add_resource(OrderList, '/order/list')

    app.run(host='::', port=config['app']['port'], debug=True)