import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import 'rxjs/add/operator/toPromise'

import { HelperService } from '@k-services/svc.helper'
import { Subject, BehaviorSubject } from 'rxjs';

/**
 * Logically treat all discount codes the same way, across all request surfaces.
 * @tutorial
 * ## Usage
 * ```
import { DiscountsService } from '@k-common/discounts/svc.discounts'

export class $className {

    // Get the basket
    this.basket = JSON.parse(this.localStorage.getItem(AppHelper.basketIdentifier))
    this.discount: number

    constructor(private _discount: DiscountsService) {}
    
    ngOnInit() {
        this._discount.request(this.basket).then((discount: number) => {
            this.discount = discount
        })
    }
}
```
 * @todo change injectable to Injectable({providedIn: 'root'}) to globalize access to the service, possible disassosiate it from the common module
 * @requires HelperService, rxjs/toPromise
 */
@Injectable()
export class DiscountsService {

    // ---- Variables ---- \\
    private _discounts: any
    private _amount: number = 0
    private _basket: any[] = []

    private _currentAddedDiscounts: any[] = []

    private _dataRules: any = undefined

    private _mountDiscountSource = new Subject()
    public mountDiscount$ = this._mountDiscountSource.asObservable()
    mountDiscount: any

    private _discountsSource = new BehaviorSubject<number>(0)
    public discount$ = this._discountsSource.asObservable()

    private _totalSource = new BehaviorSubject<number>(0)
    public total$ = this._totalSource.asObservable()

    total: number = 0
    discountByItem: Map<string, number> = new Map


    constructor(
        private _http: HttpClient,
        private _helper: HelperService
    ) {
        this.mountDiscount$.subscribe((response) => {
            this.mountDiscount = response
        })

        this.total$.subscribe((value) => {
            this.total = value
        })
    }


    // ---- Lifecycle Hooks ---- \\
    ngOnDestroy() {
        this._dataRules = undefined
    }

    // ---- Functions ---- \\

    // ---- Public

    requestByType(item) {
        return this.request([item])
    }


    /**
     * Requests a calculation of all discount codes
     * Functionality is written with a promise.all, that awaits both the HTTP request and the internal iterator
     * before pushing anything into the calculation flow
     * 
     * @public
     * @param items 
     * @returns Promise containing discount amount
     */
    public request(items): Promise<any> {

        this._basket = []
        
        items = Array.from(new Set((!!items.products ? [...items.products, ...items.discounts, ...items.individualDiscounts, ...items.misc] : items)))

        return this.executeRequests(items).then((responses: any[]) => {
            this._discounts = []
            this._amount = 0
            this._basket = items

            for(let group of responses) {

                for(let el of group) {
                    
                    this._discounts.push(el)
                }
            }
            
            this._discounts = this._orderDiscounts(this._discounts)
            this._calculateDiscounts(this._discounts)

        }).then(() => {
            this._discountsSource.next(this._amount)

            return this._amount
        })
    }


    private async executeRequests(items) {
        let httpRequestRun = await this._HttpRequestDiscounts(items)
        let isolateRun = await this._isolateDiscounts(items)

        return [httpRequestRun, isolateRun]
    }


    /**
     * Clears the mountDiscounts observable by setting the price of any object to 0
     */
    public clearMountDiscounts(code) {

        if(!!this.mountDiscount && !!this.mountDiscount.id) {

            if(this.mountDiscount.id.replace('DI_', '') == code.replace('DI_', ''))
                this._mountDiscountSource.next(undefined)
        }

    }


    setTotal(value) {
        this._totalSource.next(value)
    }

    // ---- Private

    /**
     * Requests the newest cart rules as a POST from backend.
     * This functionality has to be done every time, even though it is slow and tedious.
     * These discounts are "globalized" discounts - not coupons or individual discounts, they're on the cart
     * 
     * @private
     * @param basket
     * @returns discounts[]
     */
    private  _HttpRequestDiscounts(basket) {

        let requestActiveCartRules = {
            key: this._helper.apiKey,
            storeId: this._helper.storeId,
            websiteId: this._helper.websiteId,
            noCache: true,
            cart: basket
        }

        if(!this._dataRules) {

            return this._http.post(this._helper.server + '/feed/get/active-cart-rules', requestActiveCartRules).toPromise().then((response: any) => {
                let cartRules = response.data.rules
                this._dataRules = cartRules

                return cartRules
            })
        } else {
            return new Promise((resolve) => {

                resolve(this._dataRules)
            })
        }
    }


    /**
     * Isolates the discounts found in the cart object based on their ID
     * 
     * @private
     * @param items
     * @returns array of discounts
     */
    private _isolateDiscounts(items) {

        return new Promise((resolve) => {

            // Empties the array, to ensure a klean array on every request
            let isolatedDiscounts = []

            for(let item of items) {
                
                // finds the items that matches the `coupon` or `individuel rabat` tag
                if(!!item && !!item.id && (item.id.includes('-cu--') || item.id.includes('-ir--'))) {
                    isolatedDiscounts.push(item)
                }
            }

            resolve(isolatedDiscounts)
        })
    }


    /**
     * Orders discounts by priority, so they will iterate correctly and dont add any codes, that are lower priority that stopped flag
     * 
     * @private
     * @param discounts
     * @returns prioritizedDiscounts array[] of discounts from backend
     */
    private _orderDiscounts(discounts) {
        
        let stopped = false
        let stopPriority
        let prioritizedDiscounts: any[] = []


        // Sorts discounts by priority, so they can be iterated in that order, 1 > 2 > 3 = 3...
        let sortedDiscounts = discounts.sort((a, b) => {

            // if a has options, find the priority within the options
            if(!!a.options && a.options.find(option => option.code == 'priority')) {

                a.priority = a.options.find(option => option.code == 'priority').value

            // if a doesn't have options, but do have .data, find the priority in there
            } else if(!!a.data && a.data.priority) {

                a.priority = a.data.priority
            }

            // if b has options, find the priority within the options
            if(!!b.options && b.options.find(option => option.code == 'priority')) {

                b.priority = b.options.find(option => option.code == 'priority').value
            // if b doesn't have options, but do have .data, find the priority in there
            } else if(!!b.data && b.data.priority) {

                b.priority = b.data.priority
            }

            // Sort requires nummeric result to have effect, boolean won't work.
            return parseInt(a.priority) - parseInt(b.priority)
        })


        // Iterates discounts to validate if they're allowed to be posted or not
        for(let discount of sortedDiscounts) {
            
            // Check if the discount is stopping further propagation
            if(discount.stopped || (!!discount.data && discount.data.stopped) || (!!discount.options && discount.options.find(option => option.code == 'stopped').value == true)) {
                stopped = true
                stopPriority = discount.priority
            }

            // Push the discount if `stopped` is set and priority is above the stop-level
            if(stopped && discount.priority > stopPriority) {
                break
            } else {
                prioritizedDiscounts.push(discount)
            }
        }

        // Returns a prioritized discount element
        return prioritizedDiscounts
    }


    /**
     * Seperates discounts into their specific groups and runs the functions to calculate the prices
     * 
     * @private
     * @param discounts
     */
    private _calculateDiscounts(discounts) {

        // Iteration container, to ensure there is no duplicates added
        this._currentAddedDiscounts = []

        // Iterate all discounts, for calculating them
        for(let discount of discounts) {

            // Ensure the code is not already added in this rotation
            let id = discount.id.split('--')[1]

            if(id.includes('DI_')) {

                let idElement = id.split('_')
                id = idElement[idElement.length - 1]
            }

            if(!this._currentAddedDiscounts.includes(id)) {

                // Adds the discount to the skip list once it's been allowed to run
                this._currentAddedDiscounts.push(id)

                // If the code has an action, expect it to be a cartAction rule, else set it as an individualDiscount
                if(!!discount.action) {
                    this._cartActionDiscount(discount)
                } else {
                    this._individualDiscountCalculation(discount)
                }
            }
        }
    }


    /**
     * Calculates the discount based on the individual discounts, split in `fixed` and `percantage` calculations.
     * `percentage` takes type `discount.condition` into consideration, whereas the `fixed` isn't bothering.
     * 
     * @todo TODO: Individual discounts currently does not support multiple conditionals (aggregation), the basket element needs to be fixed for that
     * @todo TODO: Possibly add conditional discounts to `fixed` codes, though fixed codes are generally just a "writedown" of prices, so it would only be a "will it apply" rule
     * 
     * @private
     * @param discount
     */
    private _individualDiscountCalculation(discount) {

        // In case the discount code does not have `options`, move .data into it.  - /Non-homogenized content
        if(!!discount.data) {

            discount.options = []

            // Create discount.options from discount.data, but only add options if they have a value.
            for(let key of Object.keys(discount.data)) {

                if(discount.data[key].length != 0) {
                    discount.options.push({code: key, value: discount.data[key]})
                }
            }

            discount.type = 'individual-discount'
        }

        // Finds the option for which type of calculation there is used
        let type = discount.options.find(option => {
            return option['code'] === 'type'
        }).value

        switch(type) {
            case 'fixed':
            case 'by_fixed_of_base':

                if(!discount.price) {

                    // Find the amount
                    this._amount += discount.options.find(option => {
                        return option['code'] == 'price'
                    }).value
                } else {

                    // Get the amount off discount.price if it exists there
                    this._amount += discount.price
                }

                break

            case 'percentage':
            case 'percent':

                    // Find the amount for non-conditional discounts
                    let amount = discount.options.find(option => {
                        return option['code'] == 'price'
                    }).value



                    
                    // Gets the condition if it exists - /Non-homogenized content
                    let condition = discount.options.find(option => {

                        if(option['code'] == '0' || option['code'] == 'condition' || option['code'] == 'conditions') {
                            return option['code'] == '0' || option['code'] == 'condition' || option['code'] == 'conditions'
                        } else {
                            return undefined
                        }
                    })

                    // Only take the element if it has a condition and a condition.value has a length or condition.value is an object
                    if(!!condition && !!condition.value && (typeof condition.value == 'object' || condition.value.length > 0)) {

                        let amount = discount.options.find((option: any) => {
                            return option['code'] == 'price'
                        }).value


                        // Check if the condition.value is an array, so it can be iterated or not
                        if(Array.isArray(condition.value)) {

                            let accepted: boolean = true
                            let amountContainer = 0

                            // Iterate conditions if there is more than one
                            for(let iteration of condition.value) {

                                // Contains the value after a condition has been run
                                amountContainer = this._conditionalDiscount(iteration, amount)
        
                                // Set aggregation to false, if `all` flag is enabled, and one criteria returns nothing
                                // if(amountContainer === 0 && discount.aggregator == 'all') {
                                //     accepted = false
                                // }
                            }
        
                            // Check if aggregation has returned true or false
                            if(accepted) {
                                this._amount += amountContainer
                            }
                            
                        } else {
                            
                            // TODO: Add aggregation to this version at some point
                            this._amount += this._conditionalDiscount(condition.value, amount)
                        }

                    } else {


                        // Condition doesn't exist, find the discount from the total - %
                        let calculate = this.total / 100 * amount
                        this._amount += calculate
                    }

                break

                case 'mount_percentage':
                case 'mount_fixed':
                case 'mount':

                    console.warn('Mount discount has been found!', discount)

                    this._mountDiscountSource.next(discount)

                break

                default:
                    console.error('Discount was not caught!', discount)
                break;
        }
    }
    

    /**
     * Takes the condition into consideration and returns a value based on the condition
     * 
     * @private
     * @tutorial
     * ## base_sku
     * ____________
     * Base SKU will either be set to EQUAL or NOT_EQUAL.
     * 
     * EQUAL = all products with the same SKU will have to be affected
     * 
     * NOT_EQUAL = all products with a different SKU will have to be affected
     * ____________
     * 
     * @param discount
     * @returns number
     */
    private _conditionalDiscount(condition, amount, fixed: boolean = false) {

        let affectedValue = 0
        let affectedValues = new Map
        let affectedProducts = []

        // Picks the scenario for the condition to trigger
        switch(condition.attribute) {
            case 'base_sku':

                let criteria = condition.value

                for(let item of this._basket.filter((item) => !item.id.includes('-ir') && !item.id.includes('-cu') && !item.id.includes('-cd'))) {

                    if(item.sku) {

                        // Pick products to modify, based on the base_sku critera and operator, sort out the discounts and coupons
                        if(item.sku.split('-')[0] != criteria && condition.operator == 'not_equal') {
   
                            affectedProducts.push(item)
    
                        } else if(item.sku.split('-')[0] == criteria && condition.operator == 'equal') {
    
                            affectedProducts.push(item)
                            
                        }
                    }
                }

                // Loop over all product items to get their price. Discard `individual-discount`
                for(let item of affectedProducts.filter((item) => !item.id.includes('-key'))) {

                    // if item.price is empty, find the price in the options array - /Non-homogenized content
                    if(item.price == false || item.price == 'undefined' || !item.price) {

                        if(!!item.data && !!item.data.price) {

                            item.price = item.data.price
                        } else {
                            let price = item.options.find(option => option.code == 'price')

                            if(price) {

                                item.price = price.value
                            } else {

                                // Every other option is exhausted, pull the price from the backend...
                                item.price = this._http.get(this._helper.server+'feed/get/product' + this._helper.credentials + `&sku=${item.sku}&fields:price&noLocale`).toPromise().then((response: any) => {

                                    if(response == 'error') throw response
                                    else {
                                        return response.data.product.price
                                    }
                                })

                            }
                        }
                    }

                    affectedValues.set(item.id, (item.price * (item.quantity || 1) / 100 * amount))
                }

            break

            case 'curtain_type':

        
                // Find which products are affected by the filter
                for(let product of this._basket) {

                    // Ensure the product has a type_id before trying to check on it
                    if(!!product.type_id && condition.value.includes(product.type_id.toLowerCase())) {
                        affectedProducts.push(product)
                    }
                }

                if(fixed) {

                    for(let item of affectedProducts) {

                        
                        // Returns amount x quantity for each affectedProduct
                        // affectedValue = amount * (item.quantity || 1)
                        affectedValues.set(item.id, amount * (item.quantity || 1))
                    }

                } else {

                    // Loop over all product items to get their price. Discard `individual-discount`
                    for(let item of affectedProducts) {

                        // if item.price is empty, find the price in the options array - /Non-homogenized content
                        if(item.price == false || item.price == 'undefined' || !item.price) {

                            if(!!item.data && !!item.data.price) {

                                item.price = item.data.price
                            } else {
                                let price = item.options.find(option => option.code == 'price')

                                if(price) {

                                    item.price = price.value
                                } else {

                                    // Every other option is exhausted, pull the price from the backend...
                                    item.price = this._http.get(this._helper.server+'feed/get/product' + this._helper.credentials + `&sku=${item.sku}&fields:price&noLocale`).toPromise().then((response: any) => {

                                        if(response == 'error') throw response
                                        else {
                                            return response.data.product.price
                                        }
                                    })
                                }
                            }
                        }

                        affectedValues.set(item.id, (item.price * (item.quantity || 1) / 100 * amount))
                        // affectedValue = item.price * (item.quantity || 1)
                    }
                }


            break
        }
        
        
        // Get amount% of the total left
        return affectedValues.size ? Array.from(affectedValues.values()).reduce((a, b) => a + b, 0) : 0
    }


    /**
     * Checks the cart discounts based on rules. if a condition is applicable, the calculation goes through `this._conditionalDiscount()`
     * 
     * @private
     * @param discount
     */
    private _cartActionDiscount(discount) {

        switch(discount.action) {
            case 'by_percent':
            case 'by_percent_of_base':

                if(!!discount.conditions && discount.conditions.length > 0) {

                    let accepted: boolean = true
                    let amountContainer = 0

                    for(let condition of discount.conditions) {

                        // Contains the value after a condition has been run
                        amountContainer = this._conditionalDiscount(condition, discount.amount)

                        // Set aggregation to false, if `all` flag is enabled, and one criteria returns nothing
                        if(amountContainer === 0 && discount.aggregator == 'all') {
                            accepted = false
                        }
                    }

                    // Check if aggregation has returned true or false
                    if(accepted) {
                        this._amount += amountContainer
                    }

                } else {

                    // Calculate without conditional
                    this._amount += this.total / 100 * discount.amount
                }
                
                break

            case 'by_fixed':
            case 'by_fixed_of_base':

                if(!!discount.conditions && discount.conditions.length > 0) {

                    let accepted: boolean = true
                    let amountContainer = 0

                    for(let condition of discount.conditions) {

                        // Contains the value after a condition has been run
                        amountContainer = this._conditionalDiscount(condition, discount.amount, true)
                        

                        // Set aggregation to false, if `all` flag is enabled, and one criteria returns nothing
                        if(amountContainer === 0 && discount.aggregator == 'all') {
                            accepted = false
                        }
                    }

                    // Check if aggregation has returned true or false
                    if(accepted) {
                        this._amount += amountContainer
                    }

                } else {

                    // Calculate without conditional
                    this._amount += this.total + discount.amount
                }

                break;
        }
    }
}