import uuid
import datetime
import time
from decimal import Decimal, InvalidOperation

import pkg_resources
from pyramid.response import FileResponse
from pyramid.view import view_config

from sqlalchemy import desc, or_, func
import transaction

from brewman.models import DBSession
from brewman.models.master import Recipe, Product, RecipeItem, CostCentre
from brewman.models.validation_exception import TryCatchFunction, ValidationError
from brewman.models.voucher import Voucher, Inventory, VoucherType, Journal
from brewman.views.services.session import session_period_start, session_period_finish, session_period_set


@view_config(route_name='recipe_list', permission='Recipes')
@view_config(request_method='GET', route_name='recipe_id', permission='Recipes')
@view_config(request_method='GET', route_name='recipe', permission='Recipes')
def html(request):
    package, resource = 'brewman:static/base.html'.split(':', 1)
    file = pkg_resources.resource_filename(package, resource)
    return FileResponse(file, request=request)


@view_config(request_method='POST', route_name='api_recipe', renderer='json', permission='Recipes')
@view_config(request_method='POST', route_name='api_recipe_id', renderer='json', permission='Recipes')
@TryCatchFunction
def save(request):
    json = request.json_body
    recipe_product = Product.by_id(uuid.UUID(json['Product']['ProductID']))

    try:
        valid_from = datetime.date(*(time.strptime(request.json_body['ValidFrom'], '%d-%b-%Y')[0:3]))
    except (ValueError, KeyError, TypeError):
        raise ValidationError("Valid From is not a valid date")
    try:
        valid_to = datetime.date(*(time.strptime(request.json_body['ValidTo'], '%d-%b-%Y')[0:3]))
    except (ValueError, KeyError, TypeError):
        raise ValidationError("Valid To is not a valid date")

    if valid_to < valid_from:
        raise ValidationError("Valid To cannot be less than valid from")
    try:
        recipe_quantity = Decimal(json.get('Quantity', 0))
        if recipe_quantity < 0:
            raise ValidationError("Quantity must be a decimal >= 0")
    except (ValueError, InvalidOperation):
        raise ValidationError("Quantity must be a decimal >= 0")

    sale_price = 0
    if recipe_product.is_sold:
        try:
            sale_price = Decimal(json.get('SalePrice', 0))
            if sale_price < 0:
                raise ValidationError("Sale Price must be a decimal >= 0")
        except (ValueError, InvalidOperation):
            raise ValidationError("Sale Price must be a decimal >= 0")

    recipe_cost = 0
    if len(json['RecipeItems']) == 0:
        raise ValidationError("Recipe has no ingredients")
    recipe = Recipe(product_id=recipe_product.id, quantity=recipe_quantity, sale_price=sale_price,
                    valid_from=valid_from, valid_to=valid_to)
    for item in json['RecipeItems']:
        product = Product.by_id(uuid.UUID(item['Product']['ProductID']))
        quantity = round(Decimal(item['Quantity']), 2)
        if product.is_purchased:
            ingredient_cost = get_purchased_product_cost(product.id, valid_from, valid_to)
        else:
            ingredient_cost = get_sub_product_cost(product.id, valid_from, valid_to)
        cost_per_unit = ingredient_cost / (product.fraction * product.product_yield)
        recipe_cost += cost_per_unit * quantity
        recipe.recipe_items.append(RecipeItem(None, product.id, quantity, ingredient_cost))

    recipe.cost_price = round(recipe_cost / recipe_quantity, 2)
    if recipe_product.is_sold:
        recipe_product.sale_price = sale_price

    save_recipe(recipe)
    transaction.commit()
    session_period_set(request, valid_from, valid_to)
    return recipe_info(recipe.id, request)


def save_recipe(recipe):
    product = Product.by_id(recipe.product_id)
    product.price = recipe.cost_price
    update_old_rows(recipe.product_id, recipe.valid_from, recipe.valid_to, recipe.effective_from)
    DBSession.add(recipe)
    for item in recipe.recipe_items:
        item.recipe_id = recipe.id
        DBSession.add(item)


def get_purchased_product_cost(product_id, start_date, finish_date):
    quantity_sum = func.sum(Journal.debit * Inventory.quantity).label('quantity')
    amount_sum = func.sum(
        Journal.debit * Inventory.quantity * Inventory.rate * (1 + Inventory.tax) * (1 - Inventory.discount)).label(
        'amount')
    costing = DBSession.query(quantity_sum, amount_sum) \
        .join(Product.inventories).join(Inventory.voucher).join(Voucher.journals) \
        .filter(Inventory.product_id == product_id) \
        .filter(Voucher.date >= start_date) \
        .filter(Voucher.date <= finish_date) \
        .filter(Voucher.type == VoucherType.by_name('Issue').id) \
        .filter(Journal.cost_centre_id == CostCentre.cost_centre_purchase()) \
        .group_by(Product).first()
    if costing is None:
        product = Product.by_id(product_id)
        return product.price
    else:
        quantity, amount = costing
        return amount / quantity


def get_sub_product_cost(product_id, start_date, finish_date):
    product = Product.by_id(product_id)
    old_recipe = DBSession.query(Recipe).filter(Recipe.product_id == product_id) \
        .filter(Recipe.effective_to == None) \
        .filter(Recipe.valid_from == start_date) \
        .filter(Recipe.valid_to == finish_date) \
        .first()

    if old_recipe is not None:
        return old_recipe.cost_price

    old_recipe = DBSession.query(Recipe).filter(Recipe.product_id == product_id) \
        .filter(Recipe.effective_to == None) \
        .order_by(desc(Recipe.valid_to)) \
        .first()

    if old_recipe is None:
        raise ValidationError("Ingredient XXX has no recipe, cannot determine cost")

    recipe_cost = 0
    recipe = Recipe(product_id=product_id, quantity=old_recipe.quantity, sale_price=product.sale_price,
                    valid_from=start_date, valid_to=finish_date)
    for item in old_recipe.recipe_items:
        if item.product.is_purchased:
            ingredient_cost = get_purchased_product_cost(item.product_id, start_date, finish_date)
        else:
            ingredient_cost = get_sub_product_cost(item.product_id, start_date, finish_date)
        cost_per_unit = ingredient_cost / (item.product.fraction * item.product.product_yield)
        recipe_cost += cost_per_unit * item.quantity
        recipe.recipe_items.append(RecipeItem(None, item.product_id, item.quantity, ingredient_cost))

    recipe.cost_price = round(recipe_cost / old_recipe.quantity, 2)

    save_recipe(recipe)
    return recipe_cost


def update_old_rows(product_id, valid_from, valid_to, effective_date):
    old = DBSession.query(Recipe).filter(Recipe.product_id == product_id) \
        .filter(Recipe.valid_from < valid_to).filter(Recipe.valid_to > valid_from) \
        .filter(or_(Recipe.effective_to == None, Recipe.effective_to >= effective_date)) \
        .order_by(Recipe.effective_from).all()

    new_recipes = []

    for item in old:
        if item.valid_from < valid_from:
            recipe = Recipe(product_id=item.product_id, quantity=item.quantity, cost_price=item.cost_price,
                            sale_price=item.sale_price, valid_from=item.valid_from,
                            valid_to=valid_from - datetime.timedelta(days=1), effective_from=effective_date)
            for ri in item.recipe_items:
                recipe.recipe_items.append(RecipeItem(None, ri.product_id, ri.quantity, ri.price))
            new_recipes.append(recipe)
        if item.valid_to > valid_to:
            recipe = Recipe(product_id=item.product_id, quantity=item.quantity, cost_price=item.cost_price,
                            sale_price=item.sale_price, valid_from=valid_to + datetime.timedelta(days=1),
                            valid_to=item.valid_to, effective_from=effective_date)
            for ri in item.recipe_items:
                recipe.recipe_items.append(RecipeItem(None, ri.product_id, ri.quantity, ri.price))
            new_recipes.append(recipe)

        if item.effective_from == effective_date and item.effective_to is None:
            for ri in item.recipe_items:
                DBSession.delete(ri)
            DBSession.delete(item)
        else:
            item.effective_to = effective_date

    for recipe in new_recipes:
        DBSession.add(recipe)
        for item in recipe.recipe_items:
            item.recipe_id = recipe.id
            DBSession.add(item)


@view_config(request_method='DELETE', route_name='api_recipe_id', renderer='json', permission='Recipes')
def delete(request):
    recipe = DBSession.query(Recipe).filter(Recipe.id == uuid.UUID(request.matchdict['id'])).first()
    if len(recipe.product.recipes) > 1:
        DBSession.delete(recipe)
    else:
        DBSession.delete(recipe)
        DBSession.delete(recipe.product)
    transaction.commit()
    return recipe_info(None, request)


@view_config(request_method='GET', route_name='api_recipe_id', renderer='json', permission='Recipes')
def show_id(request):
    return recipe_info(uuid.UUID(request.matchdict['id']), request)


@view_config(request_method='GET', route_name='api_recipe', renderer='json', permission='Recipes')
def show_blank(request):
    return recipe_info(None, request)


@view_config(request_method='GET', route_name='api_recipe', renderer='json', request_param='list',
             permission='Authenticated')
def show_list(request):
    list = DBSession.query(Recipe).join(Recipe.product) \
        .filter(Recipe.effective_to == None) \
        .order_by(Recipe.product_id) \
        .order_by(desc(Recipe.valid_to)) \
        .all()
    recipes = []
    product_id = None
    for item in list:
        if item.product_id != product_id:
            recipe = {'ProductID': item.product_id, 'Name': item.product.name,
                      'ProductGroup': item.product.product_group.name, 'Prices': []}
            recipes.append(recipe)
            product_id = item.product_id
        costing = 0
        if item.product.is_sold and item.sale_price != 0 and item.cost_price != 0:
            costing = item.cost_price / item.sale_price
        recipe['Prices'].append(
            {'ValidFrom': item.valid_from.strftime('%d-%b-%Y'), 'ValidTo': item.valid_to.strftime('%d-%b-%Y'),
             'CostPrice': item.cost_price, 'SalePrice': item.sale_price, 'Costing': costing,
             'Url': request.route_url('recipe_id', id=item.id)})
    return recipes


def recipe_info(id, request):
    if id is None:
        info = {'Quantity': 1, 'ValidFrom': session_period_start(request), 'ValidTo': session_period_finish(request),
                'RecipeItems': []}
    else:
        recipe = DBSession.query(Recipe).filter(Recipe.id == id).one()
        info = {'RecipeID': recipe.id,
                'Product': {'ProductID': recipe.product_id, 'Name': recipe.product.name, 'Units': recipe.product.units,
                            'SalePrice': recipe.product.sale_price, 'IsSold': recipe.product.is_sold},
                'Quantity': recipe.quantity, 'SalePrice': recipe.sale_price, 'CostPrice': recipe.cost_price,
                'ValidFrom': recipe.valid_from.strftime('%d-%b-%Y'), 'ValidTo': recipe.valid_to.strftime('%d-%b-%Y'),
                'RecipeItems': []}
        for item in recipe.recipe_items:
            info['RecipeItems'].append({
                'Product': {'ProductID': item.product.id, 'Name': item.product.name, 'Units': item.product.units,
                            'FractionUnits': item.product.fraction_units, 'Fraction': item.product.fraction,
                            'ProductYield': item.product.product_yield},
                'Quantity': item.quantity, 'Price': item.price})
    return info
