// Core
import { isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';

// Helpers
import { AppHelper } from '@k-settings/app-helper'

// Services
import { SendCartFactory } from './fac.send-cart'
import { BasketService } from '@k-services/svc.basket'
import { HelperService } from '@k-services/svc.helper'

// RXJS
import 'rxjs/add/operator/toPromise'
import { Subject } from 'rxjs/Subject'
import { Subscription } from 'rxjs/Subscription'
import { Observable } from 'rxjs'

// ---- Custom Interfaces ---- \\
import { ICart, IDiscount } from '@k-core/interfaces/send-cart'
import  { ICartMessage, ICallToAction } from '../interfaces/cart-message'
import { SendCartConfiguration } from './cnf.send-cart'
import { LocalstorageService, SessionstorageService } from '@k-core/services/general/storage';
import { HttpClient, HttpParams } from '@angular/common/http';
import { server } from '@k-settings/store-helper';
import { race } from 'rxjs/observable/race';
import { PlatoConfiguratorService } from '@k-core/services/svc.plato-configurator';


@Injectable()
export class SendCartService {

    // ---- Variables ---- \\

    cart: ICart = {}
    isCartBuiltFlag = false

    private _session
    private _cartMessage: ICartMessage[]

    // Observables
    public cartSource       = new Subject<any>()
    public cart$            = this.cartSource.asObservable()

    private cartMessageSource   = new Subject<ICartMessage[]>()
    public cartMessage$         = this.cartMessageSource.asObservable()



    // Subscriptions
    private subscriptions = new Subscription()
    private keyObserver$: any

    // Intervals
    keyListener

    

    constructor(
        @Inject(PLATFORM_ID) private _platformId,
        public localStorage: LocalstorageService,
        public sessionStorage: SessionstorageService,
        private _http: HttpClient,
        private _helper: HelperService,
        private _factory: SendCartFactory,
        private _basket: BasketService,
        private _config: SendCartConfiguration,
        private _plato: PlatoConfiguratorService
    ) {
        this.subscriptions.add(
            this.cartMessage$.subscribe((res) => {
                this._cartMessage = res
            })
        )

        this.subscriptions.add(
            this._config.reset$.subscribe((state: boolean) => {
                if(state)
                    this.destroyCartSource()
            })
        )

        // Start listen
        if(isPlatformBrowser(this._platformId)) {

            this._listenForKeyChange()
        }

        // This could be a suggestion as to how to resolve this, this wont work out of the box, but could be modified to function
        
        // // Only latch on for localStorage objects if platformBrowser is accepted
        // if(isPlatformBrowser(this._platformId)) {

        //     // Assign localStorage elements to observable
        //     this.keyObserver$ = Observable.from([JSON.parse(this.localStorage.getItem(AppHelper.basketIdentifier)), sessionStorage.getItem('kake-key')])

        //     // Subscribe to the values, so they can be unsubscribed
        //     this.subscriptions.add(
        //         this.keyObserver$.subscribe((response) => {
        //             console.warn('subscribe response', response)
        //         })
        //     )
        // }

    }






    // ---- Lifecycle Hooks ---- \\
    ngOnDestroy() {
        this.subscriptions.unsubscribe()
    }


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

    // ---- Public

    /**
     * Renders items and splits them into `cart.products`, `cart.discounts` and `cart.individualDiscounts`.
     * Generates a simplified product and Sets `isCartBuiltFlag` to true
     * 
     * @returns {any[]} [ products: {}, discounts: {}, individualDiscounts: {} ]
     */
    public renderItems() {

        return new Promise((resolve) => {

            if(isPlatformBrowser(this._platformId)) {

                if(!this.cart) {
                    this.cart = {
                        products: [],
                        discounts: [],
                        individualDiscounts: [],
                        misc: [],
                    }
                }

                // Resets products, discounts and individual discounts to draw from blank canvas every time
                this.cart.products = []
                this.cart.discounts = []
                this.cart.individualDiscounts = []
                this.cart.misc = []

                setTimeout(() => {

                    // Uses async function to isolate all elements, then add them to the correct parts of the cart
                    this._executeRenderItems().then((response) => {
                        

                        this.cart.discounts = this._helper.filter_array(response.discounts)
                        this.cart.individualDiscounts = this._helper.filter_array(response.individualDiscounts)
                        this.cart.misc = this._helper.filter_array(response.miscellaneous)
    
                        // Uses async function to render all products
                        this._renderProducts(response.products).then(() => {
    
                            this.isCartBuiltFlag = true
                            this.cartSource.next(this.cart)
    
                            // Resolves when all tasks are completed
                            resolve(this.cart)
                        })
                    })
                }, 500)

            }
        })
    }


    public getCustomers() {

        const obj: HttpParams = new HttpParams()
            .set('key', this._helper.apiKey)
            .set('websiteId', this._helper.websiteId.toString())
            .set('storeId', this._helper.storeId.toString())
            .set('noCache', 'true')

        return this._http.get(`${server}/feed/get/customers`, {params: obj})
    }


    /**
     * Requests the product label from `_factory`
     * 
     * @param label 
     */
    requestProductLabel(label) {

        return this._factory.requestType(label).then((response: any) => 

            response.data.attributes.type_id
        )
    }


    /**
     * Sets all carrier info on the `cart$`
     * 
     * @param carrier_type string
     * @param type string
     * @param price number
     */
    setShipping(carrier_type, type, price) {

            let interval = setInterval(() => {
                if(!!this.cart) {

                    this.cart.shipping = {method: carrier_type, cost: price}
            
                    this.cartSource.next(this.cart)
                    clearInterval(interval)
                }
            }, 100)
            
    }


    nullifyShipping() {
        (this.cart.shipping as any) = 'none'
        this.cartSource.next(this.cart)
    }

    /**
     * Extends shipping info with store_id
     * 
     * @param id 
     */
    setExtendedShipping(store) {

        let int = setInterval(() => {
            if(('shipping' in this.cart)) {

                clearInterval(int)
                this.cart.shipping.store = store

                this.cartSource.next(this.cart)
            }
        }, 100)

        // if(!('shipping' in this.cart)) (this.cart.shipping as any) = {}
    }


    /**
     * Sets the block content coming from Bus block
     * 
     * @param busId 
     * @param content 
     */
    setBusBlockContent(busId, content) {

        if(!!this.cart) {

            this.cart.bus = {
                id: busId,
                ref: content
            }
    
            this.cartSource.next(this.cart)
        }
    }


    /**
     * Sets discounts in `cart$`
     * 
     * @param discounts 
     */
    setDiscounts(discounts: IDiscount[]) {

        // Deep clone cart
        let localCart = JSON.parse(JSON.stringify(this.cart))

        for(let discount of discounts) {
            if(discount.id.includes('-ir--DI_')) {
                let index = localCart.individualDiscounts.findIndex(individual => individual.id.replace('-ir--', '') === discount.id.replace('-ir--DI_', '').split('_')[1])
                delete localCart.individualDiscounts[index]

                localCart.individualDiscounts = []

            }
        }

        localCart.discounts = discounts
        this.cart.discounts = localCart.discounts

        this.cart.individualDiscounts = localCart.individualDiscounts
        this.cartSource.next(this.cart)
    }


    /**
     * Sets comment in `cart$`
     * 
     * @param message 
     */
    setComment(message) {
        if(!!this.cart) {

            this.cart.comment = { 'message': message }
    
            this.cartSource.next(this.cart)
        }
    }


    /**
     * Sets Mutual Agreement in `cart$`
     * 
     * @param message 
     */
    setMutualAgreement(message) {

        if(!!this.cart) {

            this.cart.mutualAgreement = { 'message': message }
    
            this.cartSource.next(this.cart)
        }
    }

    
    /**
     * Sets the `salesRep` on the cart element and pushes it into the source
     * 
     * @param message 
     */
    setSalesRep(message) {

        if(!!this.cart) {

            this.cart.salesRep = message
    
            this.cartSource.next(this.cart)
        }
    }


    /**
     * sets address in `cart$`
     * 
     * @param id billing | shipping
     * @param address Object of address
     */
    setAddress(id, address) {
        this.cart[id] = address
        
        this.cartSource.next(this.cart)
    }


    destroyCartSource() {

        this.cart = undefined
        this.cartSource.next([])
    }


    private _sessionStorageIdentifier = 'checkout'

    /**
     * Works somewhat as a Map of SessionStorage. This allows us to put values into
     * session storage, and retrieve them, in a decent manner, across all components
     * within the service
     * 
     * @param {string} state get | set | has | delete | clear
     * @param {string} identifier container for value
     * @param {any} value any
     */
    sessionStore(state, identifier?, value?) {

        if(isPlatformBrowser(this._platformId)) {

            switch(state) {
                case 'get':
                    return this._sessionStorageGetter()
                
                case 'set':
                    this._sessionStorageSetter(identifier, value)
                    break;
    
                case 'has':
                    return this._sessionStorageHas(identifier)
                    
                case 'delete':
                    this._sessionStorageDelete(identifier)
                    break;
    
                case 'clear':
                    this._sessionStorageClear()
                    break;
    
                default:
                    console.error('please input "get | set | has | delete | clear"')
                    break;
            }
        }
    }
    




    // ---- Private

    /**
     * Gets Session Storage
     */
    private _sessionStorageGetter() {

        
        this._session = JSON.parse(this.localStorage.getItem(this._sessionStorageIdentifier))

        if(!this._session)
            this._session = {}

        return this._session
    }


    /**
     * Sets Session Storage
     * 
     * @param {string} identifier 
     * @param {any} value 
     */
    private _sessionStorageSetter(identifier, value) {

        this._session[identifier] = value

        this.localStorage.setItem(this._sessionStorageIdentifier, JSON.stringify(this._session))
    }


    /**
     * Has session storage
     * 
     * @param identifier
     * @return boolean
     */
    private _sessionStorageHas(identifier) {

        if(!!this._session && !!this._session[identifier] && this._session[identifier].length)
            return true
        else
            return false
    }


    /**
     * Deletes session storage
     * 
     * @param identifier 
     */
    private _sessionStorageDelete(identifier) {
        delete this._session[identifier]
        this.localStorage.setItem(this._sessionStorageIdentifier, JSON.stringify(this._session))
    }


    /**
     * Clears the Session Storage
     */
    private _sessionStorageClear() {
        this.sessionStorage.removeItem('kake-discounts')
        this.localStorage.removeItem(this._sessionStorageIdentifier)
    }



    /**
     * Add a new message to message observable with type, message, and optional cta
     * @param type - `error` | `warning`
     * @param message 
     * @param cta - type can be the following: `button` | `link`
     */
    _addMessage(type: string, message: string, cta? : ICallToAction ) {
        let messages: ICartMessage[] = this._cartMessage ? this._cartMessage : []
        let new_message: ICartMessage = {
            type: type, 
            message: message,
            cta: cta ? cta : null
        }

        // Add new message to array
        messages.push(new_message)

        // Update observable
        this.cartMessageSource.next(messages)
    }


    /**
     * Continuously compare session storeage and local storage keys and clear session storage
     * if keys are different. Also show an error message to let the user know.
     */
    private _listenForKeyChange() {

        if(isPlatformBrowser(this._platformId)) {

            this.keyListener = setInterval(() => {

                let cartKey: string

                // Get cart
                let localCart = this.localStorage.getItem(AppHelper.basketIdentifier)

                if(!!localCart) {


                    // Get session key
                    let sessionKey = this.sessionStorage.getItem('kake-key')
    
                    if(!!sessionKey) {
                        // find key in local
    
                        cartKey = JSON.parse(localCart).find((item) => {
                            return item.id.indexOf('-key--') > -1
                        }).id.replace('-key--', '')
    
                        // if key is different clear session and interval, and show error message
                        if(!!cartKey && cartKey != sessionKey) {
                            
                            // this.sessionStore('clear')

                            this._addMessage('error', 'the session has been lost', {type: 'button', label: 'please go back to the cart', url: '/'})
                            clearInterval(this.keyListener)

                        } else {
    
                        }
                    }
                }


            }, 500)
        }
    }


    // ---- renderItems private functions
    private async _executeRenderItems() {

        // Awaits local cart
        let localCart = await this._getCart()

        // Isolate products
        let products = await this._mapProducts(localCart)

        // Isolate coupons
        let discounts = await this._mapRenderRequest(localCart, '-cu')

        // Isolate individual discount
        let individualDiscounts = await this._mapRenderRequest(localCart, '-ir')

        // Find miscellaneous elements, and keep them in a seperate object
        let miscellaneous = await this._mapRenderRequest(localCart, '-info')

        // Returns all results as an object, so each part can be picked out individually
        return {products: products, discounts: discounts, individualDiscounts: individualDiscounts, miscellaneous: miscellaneous}
    }


    // Getters 
    
    /**
     * Describe...
     */
    private async _getCart() {

        return await JSON.parse(this.localStorage.getItem(AppHelper.basketIdentifier))
    }


    /**
     * Describe...
     */
    private _mapProducts(cart) {
        return new Promise((resolve) => {

            let cartMap = cart.map((element) => {

                // Isolate the elementId for DRYness
                let elementId = element.id.toLowerCase()

                // Check if elements id begins with any of the product logic
                if(elementId.startsWith('-conf') || elementId.startsWith('-sku') || elementId.startsWith('-dv') || elementId.startsWith('-plato')) {
                    return element
                }
            })

            resolve(cartMap)
        })
    }


    /**
     * Describe...
     */
    private _renderDiscounts(cart) {

        let discounts = []
        let errors = 0

        return new Promise((resolve, reject) => {
            discounts.push(
                cart.find((element) => {

                    if(element.id.includes('-cu') || element.id.includes('-ir')) {

                        return element
                    }
                })
            )

            let interval = setInterval(() => {

                if(this._helper.filter_array(discounts).length == this._helper.filter_array(cart.map(item => item.id.includes('-cu') || item.id.includes('-ir'))).length) {
                    clearInterval(interval)
                    resolve(this._helper.filter_array(discounts))
                }
                else {

                    errors++

                    if(errors == 10) {


                        clearInterval(interval)
                        resolve(this._helper.filter_array(discounts))
                    }

                }
            }, 200)
        })
    }


    /**
     * Describe...
     */
    private _mapRenderRequest(cart, request) {

        return new Promise((resolve) => {
            resolve(
                cart.map(element => {
                    if(element.id.includes(request))
                        return element
                })
            )
        })
    } 


    // ---- renderProducts private functions

    /**
     * Renders products seperately from `this.renderItems()`
     * 
     * @param products 
     */
    private _renderProducts(products) {

        products = this._helper.filter_array(products)
        let i = 0
        return new Promise((resolve) => {

        
            // Sets product to a minimal version
            for(let product of products) {

                let id = product.id
                
                // If product has already been loaded, data.name has been moved to product.sku
                let sku = !!product.data ? (product.id.includes('-sku--') ? product.id.replace('-sku--', '') : product.data.name) : product.data.name

                // Sequentially executes async promises, to ensure they're all completed before the result is returned.
                this._executeProducts(product, id, sku).then(() => {

                    i++

                    if(i == products.length) {
                        console.warn('executed all products', this.cart)
                        resolve(true)
                    }
                })
            }
        })
    }


    /**
     * async function to make async wait for each other before continuing
     * 
     * @param product 
     * @param id 
     * @param sku 
     */
    private async _executeProducts(product, id, sku) {

        const productId = product.id.toLowerCase()

        if(productId.startsWith('-conf-')) {
                    
            let productRun = await this._getConfigurationFromBasket(product, id, sku)
        } else if(productId.startsWith('-dv-')) {
            
            let productRun = await this._getDiverseFromBasket(product, id, sku)

        } else if(productId.startsWith('-sku-')) {

            let productRun = await this._getSkuFromBasket(product, id, sku)
        } else if(productId.startsWith('-plato-')) {
            
            let productRun = await this._getProductFromExternal(product) 
        }
    }

    private async _getProductFromExternal(product) {

        let element

        return this._plato.getVariant(product.data.guid)
            .then((response: any) => {

                element = {
                    id: product.id,
                    name: product.data.title,
                    sku: product.data.guid,
                    type_id: 'plato',
                    height: 0,
                    width: 0,
                    price: response.price,
                    quantity: product.quantity
                }

                this.cart.products.push(element)
            })
    }

    // Getters

    /**
     * Gets the configurable data, and the product label, and mutates them to fit within the boundries set by the components
     * 
     * @param product 
     * @param id 
     * @param sku 
     */
    private _getConfigurationFromBasket(product, id, sku) {

        let element

        return new Promise((resolve) => {
            this._basket.getBasketItem(product).then((response) => {
                
                // Finds width
                let width = response.options.find(opt => {
                    if(opt.code == 'width')
                        return opt
                })

                // Finds height
                let height = response.options.find(opt => {
                    if(opt.code == 'height')
                        return opt
                })
            

                // Define the product and get product type_id attribute
                this.requestProductLabel(response.type_id).then((name) => {

                    element = {
                        name: name,
                        id: id,
                        sku: response.sku ? response.sku : 'curtain',
                        type_id: response.type_id,
                        height: !!height ? height.value : 0,
                        width: !!width ? width.value : 0,
                        price: response.price,
                        quantity: response.quantity
                    }

                    this.cart.products.push(element)
                    resolve(true)
                })

            })
        })
    }


    /**
     * Gets the DV product data, and mutates it to match
     * 
     * @param product 
     * @param id 
     * @param sku 
     */
    private _getDiverseFromBasket(product, id, sku) {

        let element

        return new Promise((resolve) => {

            // Sets a Diverse product
            element = {
                id: id,
                sku: 'misc',
                type_id: !!product.data ? product.data.title : product.type_id,
                quantity: product.quantity,
                price: !!product.data ? product.data.price : product.price,
            }
            
            this.cart.products.push(element)
            resolve(true)
        })
    }


    /**
     * Gets the SKU product data, and mutates it to match
     * 
     * @param product 
     * @param id 
     * @param sku 
     */
    private _getSkuFromBasket(product, id, sku) {

        let element

        return new Promise((resolve) => {

            // Gets sku product and parses it to this.cart
            this._factory.getSkuProductPrice(product.id.replace('-sku--', '')).then((response: any) => {
                let responseProduct = response.data.product

                element = {
                    id: id,
                    sku: sku,
                    type_id: responseProduct.name,
                    quantity: product.quantity,
                    price: parseFloat(responseProduct.price)
                }

                this.cart.products.push(element)
                resolve(true)
            })
        })
    }
}