import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { TransferState, makeStateKey } from '@angular/platform-browser'

import 'rxjs/add/operator/toPromise'
import { Subject } from 'rxjs/Subject'
import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators'

import { HelperService }        from '@k-services/svc.helper'
import { SharedService }        from '@k-services/svc.shared'
import { CacheService }         from '@k-services/general/svc.cache'
import { retry, delay } from 'rxjs/operators'


const PARENTSKEY = makeStateKey('configuratorParentsKey')
const CHILDRENKEY = makeStateKey('configuratorChildrenKey')
const MEASUREKEY = makeStateKey('configuratorMeasurementKey')

interface Options {
    "color-family"?: string;
    "details": any; // All details, too many to list
    "measurements": {
        "height": number;
        "weidth": number;
        "a": number;
        "b": number;
        "c": number;
        "d": number;
        "angle": number;
    }
    "dessin": string;
    "type_id": string;
}

interface ISpec {
    id: string;
    sort: number;
    values: [any]
}

@Injectable()
export class ConfiguratorService {
    
    // ---- Variables ---- \\
    // States
    stateDestroyed

    // Data
    currentSpec
    currentProduct
    price
    // Spec Map Interface (translations)
    specsMap = new Map

    configuratorType: any
    private populateProductTimeout: ReturnType<typeof setTimeout>


    // ---- Observables ---- \\
    private productSource = new ReplaySubject<any>()
    public product$ = this.productSource.asObservable()

    private priceSource = new Subject<any>()
    public price$ = this.priceSource.asObservable()

    private specsSource = new Subject<any>()
    public spec$ = this.specsSource.asObservable()

    private modalSource = new Subject<any>()
    public modal$ = this.modalSource.asObservable()

    private resetSource = new Subject<any>()
    public reset$ = this.resetSource.asObservable()

    private validSource = new BehaviorSubject<boolean>(false)
    public valid$ = this.validSource.asObservable()


    private validationResultsSource = new Subject<any>()
    public validationResults$ = this.validationResultsSource.asObservable()

    private specSelectorSource = new Subject<any>()
    public specSelector$ = this.specSelectorSource.asObservable()

    // Configurator product specificaiont/type, color selector validation
    private productValidationSource = new Subject<any>()
    public productValidation$ = this.productValidationSource.asObservable()

    private mountMethodSource = new Subject<string | number>()
    public mountMethod$ = this.mountMethodSource.asObservable()





    constructor(
        private _http: HttpClient,
        private _helper: HelperService,
        private _shared: SharedService,
        private _state: TransferState,
        private _cache : CacheService
    ) {
        // Subscribe to Spec
        this.spec$.subscribe((response) => {
            this.currentSpec = response
        })

        // Subscribe to Product
        this.product$.subscribe((response) => {
            this.currentProduct = response
        })

        this.price$.subscribe((response) => {
            this.price = response
        })
    }






    // ---- Functions ----- \\
    set mountMethod(method) {
        this.mountMethodSource.next(method)
    }

    private handleError(error: any): Promise<any> {
        console.error('An error occurred', error); 
        return Promise.reject(error.message || error);
    }


    // SET

    // selector to select Measring, parent, tpye coloe from menu e.g in Suntex case
    setSpecSelector(value) {
        this.specSelectorSource.next(value)
    }

    setProductValidation(value) {
        this.productValidationSource.next(value)
    }

    setSpecs(value) {
        this.specsSource.next(value)
    }

    setPrice(value) {
        this.priceSource.next(value)
    }




    /**
     * Update `productSource` with `values` and request price based on same values.
     * Request to price is set in a timeout which is cleared when method is called again to reduce spam.
     * 
     * @param values 
     */
    populateProduct(values) 
    {
        // Update product source with values
        this.productSource.next(values)


        // Reset price request to reduce spam
        if(this.populateProductTimeout) {
            clearTimeout(this.populateProductTimeout)
        }


        // Wait 500ms then request and set price
        this.populateProductTimeout = setTimeout(() => {

            this.requestPrice(values).then(response => {
                this.setPrice(response)
            }) 
        }, 500)
    }


    setModal(value:boolean) {
        this.modalSource.next(value)
    }

    
    // Used to trigger reset, remember to set back false
    setReset(value:boolean) {
        this.resetSource.next(value)

    }



    /**
     * GETs parent types, then returns data.parents to frontend
     * @todo Insert `TransferState`, and get it to work correctly. on recent attempts it does not bring back the full data
     */
    getParents(): Promise<any[]> {

        let KEY = PARENTSKEY

        let stateValue = this._state.get(KEY, null as any)
        let cacheId: string = 'configurator-parent'
        let requestXHR = this._helper.server + 'feed/get/configurator-parents' + this._helper.credentials

        if(this._cache.has(cacheId)) {

            return new Promise(resolve => {

                resolve(this._cache.get(cacheId))

            }).then((response:any) => {
                
                if(this._state.hasKey(KEY))
                    this._state.remove(KEY)

                return response
            })

        } else {

            if(!stateValue) {

                return this._http.get(requestXHR).toPromise()
                    .then((response: any) => {
                        
                        if(response.status !== 'success' || response.status == 500) throw response
                        else {
                            let data = response.data.parents
                            
                            // Set the state
                            this._state.set(KEY, data)
    
                            // Sets the cache value
                            this._cache.set(cacheId, data)
    
                            return data
                        }
                }).catch((err) => {
    
                    if(err.status == 500)
                        this._helper.logRequestError(err.statusText, 'GET', requestXHR, '/app-core/modules/custom-product/services/svc.custom-product.ts')
                    else
                        this._helper.logRequestError(err.data.errorMessage, 'GET', requestXHR, '/app-core/modules/custom-product/services/svc.custom-product.ts')
    
                })

            } else {
                return new Promise(resolve => {

                    resolve(stateValue)

                }).then((response: any) => {

                    
                    if(this._state.hasKey(KEY))
                        this._state.remove(KEY)

                    this._cache.set(cacheId, response)
                    
                    return response
                })
            }

        }
    }



    /**
     * GETs children based on which parent is selected
     * puts from the Cache if available, else pulls from TransferState else from API
     * If request fails with either `error` or `500`, catch the error and display as a caught message
     * 
     * @todo fix state, it is disabled because it causes issues
     * 
     * @param parent 
     */
    getChildren(parent) {
        
        let KEY = CHILDRENKEY

        let value = this._state.get(KEY, null as any)
        let cacheId: string = 'configurator-type' + '_' + parent
        let requestXHR = this._helper.server + 'feed/get/configurator-type' + this._helper.credentials + '&type_id=' + parent

        if(this._cache.has(cacheId)) {

            return new Observable((observer) => {


                let data = this._cache.get(cacheId)

                observer.next(!!data.is_sunway ? {platoConfigurator: true} : data.children)
            }).pipe(
                tap(() => {
                if(this._state.hasKey(KEY))
                    this._state.remove(KEY)
                })
            )

        } else {


            return this._http.get(requestXHR)
                .pipe(
                    map((response: any) => {
                        if(response.status !== 'success' || response.status == 500) throw response
                        else return response.data.type
                    }),
                    map((data: any) => {

                        // Sets the cache value
                        this._cache.set(cacheId, data)

                        this._state.set(KEY, data)

                        return !!data.is_sunway ? {platoConfigurator: true} : data.children
                        
                    }),
                    catchError((err) => {

                        if(err.status == 500)
                            this._helper.logRequestError(err.statusText, 'GET', requestXHR, '/app-core/modules/custom-product/services/svc.custom-product.ts')
                        else
                            this._helper.logRequestError(err.data.errorMessage, 'GET', requestXHR, '/app-core/modules/custom-product/services/svc.custom-product.ts')
                        
                        return err

                    })
                )
        }
    }

    /**
     * GETs measurement data based on currently selected `type_id`
     * puts from the Cache if available, else pulls from TransferState else from API
     * If request fails with either `error` or `500`, catch the error and display as a caught message
     * 
     * @param parent 
     */
    getMeasurements(type_id: string): Observable<any> {

        let requestXHR = this._helper.server + 'feed/get/configurator-typemeasurements' + this._helper.credentials + '&type_id=' + type_id

        return this._http.get(requestXHR)
            .pipe(
                map((response: any) => {

                    if(response.status !== 'success' || response.status == 500) throw new Error('Response for getMeasurements failed:' + response)
                    else return response.data
                }),
                delay(500)
            )
    }

    
    /**
     * GETs specific color data based on a dessin_id parsed
     * 
     * @param id 
     */
    getColorSpecs(id, type_id?) {

        let cacheId = 'configurator_dessin' + '_' + id + (type_id ? '_' + type_id : '')

        if(this._cache.has(cacheId)) {
            return new Promise((resolve) => {
                resolve(this._cache.get(cacheId))
            }).then((response: any) => {
                return response
            })

        } else {

        return this._http.get(this._helper.server + 'feed/get/configurator-dessin' + this._helper.credentials + '&dessin_id=' + id + (!!type_id ? '&type_id=' + type_id : '')).toPromise()
            .then((response: any) => {

                this._cache.set(cacheId, response)
                return response
            })
        }

    }
    


    /**
     * GETs chain length data based on type id, height and mountheight. Configured to use the cache with same criteria
     * TODO: Re-add caching, in a more unobstructive way
     * 
     * @param type_id 
     * @param height 
     * @param mountHeight 
     */
    getChainLength(type_id, height, mountHeight, method?): Observable<any> {

        return this._http.get(this._helper.server + 'feed/get/configurator-chainlength' + this._helper.credentials + '&type_id=' + type_id + '&height=' + height + '&mountHeight=' + mountHeight + (!!method ? '&mountMethod='+method : ''))
    }



    /**
     * POSTs order data to retrieve a price
     * 
     * @param options 
     */
    requestPrice(options: any) {

        let object = {
            noCache: true,
            key: this._helper.apiKey,
            storeId: this._helper.storeId,
            websiteId: this._helper.websiteId,
            options: options
        }

        let cacheId = options

        if(this._cache.has(options)) {
            
            return new Promise((resolve) => {
                resolve(this._cache.get(cacheId))
            }).then((response: any) => {
                return response
            })
        } else {

            return this._http.post(this._helper.server + 'feed/get/configurator-price', object).toPromise()
                .then((response: any) => {
    
                    if(response.status == 'error') throw response
    
                    return response
    
            }).catch((error) => {
                console.error({type: 'POST', url: 'feed/get/configurator-price', message: error.data.errorMessage})
                return []
            })
        }

    }

    

    /**
     * POSTs all options from a configurable to get translated version
     * 
     * @param options 
     * @returns Promise
     * 
     * TODO: Cache attributes individually and return cached it same. Use local "cache" (Map) to avoid bloating LRU
     */
    requestLabel(options: Options) {

        let cacheKeyIdentifier = options.type_id + options["color-family"] + JSON.stringify(options.details) + options.dessin

        let cacheId = 'config-label-' + cacheKeyIdentifier

        let object = {
            noCache: true,
            key: this._helper.apiKey,
            storeId: this._helper.storeId,
            websiteId: this._helper.websiteId,
            attributes: options
        }


        if(this._cache.has(cacheId)) {
            return new Promise((resolve) => { resolve(this._cache.get(cacheId)) })
        } else {
            
            return this._http.post(this._helper.server + 'feed/get/configurator-labels', object).toPromise()
    
                .then((response: any) => {
                    if(response.status == 'error') throw response
                    
                    this._cache.set(cacheId, response)
                    return response
                }).catch((error) => {
                    console.error({type: 'POST', url: 'feed/get/configurator-labels', message: error.data.errorMessage})
                    return []
                })
        }
    }


    /**
     * Checks if value is set or not, also has a switch case to check for custom validity.
     * 
     * Will set both `valid$` and `validationResults$` observables with results
     * 
     * @param defaults - Array of default values to return
     * @param ignore - array of what to ignore
     */    
    validate(systemDefaults: ISpec[] = [], ignore: string[] = []) {


        new Promise((resolve) => {
            if(this.currentSpec != undefined)
                resolve(this.currentSpec)

        }).then((response: ISpec[]) => {

            let specs:ISpec[] = response
            let product     = this.currentProduct
            let resultMap   = new Map

            // Deep cloning system defaults, to sever the link, so it does not get cross contamination
            let defaults    = JSON.parse(JSON.stringify(systemDefaults))

            if(!!specs) {
                // compare specs to ignored list and push into defaults
                for(let value of specs) {
                    if(!ignore.includes(value.id)) {
                        defaults.push(value)
                    }
                }
            }
        
            for(let def of defaults) {
                // Set flag
                let productValue = undefined

                // Set the value
                if(!!product && !!product[def.id]) {
                    productValue = product[def.id]
                }

                if((!!productValue || (def.id == 'mountHeights' && productValue == null))) {
                    
                    // custom logic for validation
                    switch(def.id) {

                        // MEASUREMENTS
                        case "measurements":
                            let measurementState: boolean = true

                            for(let object of Object.keys(product[def.id])) {
                                
                                if(productValue[object] === null)
                                    measurementState = false
                                
                                // Do not confirm angle above 55 deg or below 0
                                if(object === 'angle' && (productValue[object] >= 55 || productValue[object] < 0))
                                    measurementState = false

                                // Do not accept a C value above 500
                                if(object === 'c' && productValue[object] > 500)
                                    measurementState = false
                            }

                            resultMap.set(def.id, measurementState)
                            break


                        // DETAILS
                        case "details":

                            let detailState: boolean = true

                            for(let object of Object.keys(product[def.id])) {
                                if(productValue[object] === undefined)
                                    detailState = false

                                resultMap.set(def.id, detailState)
                            }
                            break


                        // TYPE
                        case "type_id":

                            // Only validate if type_id does not contain `_SUPER`
                            if (productValue.length > 0 && !productValue.includes('_SUPER')) {
                                resultMap.set(def.id, true)
                            } else {
                                resultMap.set(def.id, false)
                            }
                            break

                        // MOUNTHEIGHT + MOUNTTYPE + CHAINLENGTH
                        case "mountHeights":

                            if(product.MountType == 'Childsafe') {

                                if(product.mountHeights == 0) {

                                    resultMap.set(def.id, false)
                                } else {
                                    
                                    resultMap.set(def.id, true)
                                }
                            } else {
                                resultMap.set(def.id, true)
                            }

                            break;

                        // DEFAULT
                        default:
                            resultMap.set(def.id, true)
                            break
                    }
                }
                else {
                    resultMap.set(def.id, false)
                }


                // Push new results to observable
                this.validationResultsSource.next(resultMap)



                // Check valid state
                let result = Array.from(resultMap)
                let isAccepted = true

                for(let res of result) {
                    if(!res[1]) {
                        isAccepted = false
                    }
                }

                // If store is not Suntex
                if(this._helper.storeId !== 19) {

                    // If price returns false, make invalid
                    if(!!this.price && this.price.data.price === false) {
    
                        isAccepted = false
                    }
                }


                this.validSource.next(isAccepted)
            }
            
            
            // To configure a product all input send and used for validation
            this.setProductValidation(resultMap)
        })

    }


    /**
     * Generates a key:value pair of all child values, of the specs.
     * Specs cover seperate element values on a configuration (Dessin, Details etc.)
     * 
     * @param specs 
     */
    generateSpecMap(specs) {

        // if(!this.specsMap.has(specs.type_id))

        for(let spec of specs) {

            let typeId = ''

            if(typeof spec === 'object') {


                typeId = spec.id

                if(!!spec.values && spec.values.length > 0) {
                    
                    spec.values.find((el) => {

                        if(el.values) {

                            el.values.find((child) => {
                                let specId = child[(typeId == 'details' ? 'detail' : typeId ) + '_id']
                                let specName = child['name']
                                if(!this.specsMap.has(specId))
                                this.specsMap.set(specId, specName)
                            })
                        }
                    })
                }
            }
        }
    }

    addSingleValueSpecMap(key, value) {
        if(!this.specsMap.has(key))
            this.specsMap.set(key, value)
    }
}