define([
    'durandal/system',
    'services/model',
    'services/datacontext.storage',
    'services/appsecurity',
    'config',
    'services/logger',
    'Q',
    'breeze',
    'knockout',
    'jquery',
    'services/breeze.knockout', 'breeze-labs/breeze.saveErrorExtensions'],
    function(system, model, DataContextStorage, appsecurity, config, logger, Q, breeze, ko, $){

        // Use knockout library
        breeze.core.config.initializeAdapterInstances({ modelLibrary: "knockout" });
        // get the current default Breeze AJAX adapter
        var ajaxAdapter = breeze.config.getAdapterInstance("ajax");
        // set fixed headers
        ajaxAdapter.defaultSettings = {
            headers: appsecurity.getSecurityHeaders()
        };

        // Init Entity Manager
        var manager = initEntityManager();

        // Query helper
        var EntityQuery = breeze.EntityQuery;
        var entityNames = model.entityNames;
        var orderBy = model.orderBy;
        var storage = new DataContextStorage(manager);


        var hasChanges = ko.observable(false);

        var cancelChanges = function () {
            manager.rejectChanges();
            log('Canceled changes', null, true);
        };

        var saveChanges = function () {
            return manager.saveChanges()
                .then(saveSucceeded)
                .fail(saveFailed);

            function saveSucceeded(saveResult) {
                log('Saved data successfully', saveResult, true);
                storage.save('saved result');
            }

            function saveFailed(error) {
                var msg = 'Save failed: ' + breeze.saveErrorMessageService.getErrorMessage(error);
                error.message = msg;
                logError(msg, error);
                throw error;
            }
        };

        var primeData = function () {
            var hasData = storage.load(manager);
            var promise = hasData ? Q.resolve(log('Validators already applied')) : Q.all([getLookups()]).then(applyValidators);

            return promise.then(success);

            function success() {
                datacontext.lookups = { // Export it
                    oils: getLocal('Oils', 'name', true),
                    tags: getLocal('Tags', 'name', false)
                };
                log('Primed data', datacontext.lookups);
            }

            function applyValidators() {
                model.applyValidators(manager.metadataStore);
            }
        };

        manager.hasChangesChanged.subscribe(function (eventArgs) {
            // update the datacontext observable 'hasChanges'
            // based on the manager's latest 'hasChangesChanged' event state.
            // hasChanges() is the method
            // hasChangesChanged is the event
            hasChanges(eventArgs.hasChanges);
        });

        //# HEALTH
        var getHealthPartials = function(healthsObservable, forceRemote, where) {
            if (!forceRemote) {
                var s = getLocal('Healths', orderBy.health, false, where);
                if (s.length > 0) {
                    healthsObservable(s);
                    return Q.resolve();
                }
            }

            var query = EntityQuery
                .from('Healths')
                .expand(['diseases', 'diseases.tags', 'diseases.directions', 'diseases.directions.oil', 'diseases.tags.tag'])
                .orderBy(orderBy.health)
            ;

            if(where){
                query =  query.where(where);
            }

            return manager.executeQuery(query)
                .then(querySucceeded)
                .fail(queryFailed);

            function querySucceeded(data) {
                var list = data.results;
                if (healthsObservable) {
                    healthsObservable(list);
                }
                storage.save(entityNames.health);
                log('Retrieved [Health Partials] from remote data source', list, true);
            }

        };
        var getHealthById = function (healthId, healthObservable) {

            var query = EntityQuery.from('Healths')
                .where('id', 'eq', healthId)
                .expand(['diseases', 'diseases.tags', 'diseases.directions', 'diseases.directions.oil', 'diseases.tags.tag']);

            return manager.executeQuery(query).then(fetchSucceeded).fail(queryFailed);

           // return manager.fetchEntityByKey(entityNames.health, healthId, true).then(fetchSucceeded).fail(queryFailed);

            // 2nd - Refresh the entity from remote store (if needed)
            function fetchSucceeded(data) {
                var s = data.results[0];

                // if the entity we got is partial, we need to go get remote
                if (s.isPartial()) {
                    return refreshHealth(s);
                } else {
                    var src = data.fromCache ? ' from cache.' : ' from remote data source.';
                    log('Retrieved [Health] id:' + s.id() + src, s, true);
                    if (!data.fromCache) {
                        storage.save('[Health] id:' + s.id());
                    }
                    return healthObservable(s);
                }
            }

            function refreshHealth(health) {
                return EntityQuery.fromEntities(health)
                    .using(manager).execute()
                    .then(querySucceeded).fail(queryFailed);
            }

            function querySucceeded(data) {
                var s = data.results[0];
                log('Retrieved [Health] id:' + s.id() + ' from remote data source', s, true);
                storage.save('fresh [Health] w/ id:' + s.id());
                return healthObservable(s);
            }
        };
        var createHealth = function () {
            return manager.createEntity(entityNames.health, {isPartial: false});
        };
        //# END HEALTH

        //# DISEASE
        var createDisease = function (health) {
            return manager.createEntity(entityNames.disease, {isPartial: false, health: health});
        };
        var createDirection = function (disease) {
            return manager.createEntity(entityNames.direction, {isPartial: false, disease: disease});
        };
        //# END DISEASE

        //# TAG
        var getTagById = function (tagId, tagObservable) {
            return manager.fetchEntityByKey(entityNames.tag, tagId, true).then(fetchSucceeded).fail(queryFailed);

            // 2nd - Refresh the entity from remote store (if needed)
            function fetchSucceeded(data) {
                var s = data.entity;
                // if the entity we got is partial, we need to go get remote
                if (s.isPartial()) {
                    return refreshTag(s);
                } else {
                    var src = data.fromCache ? ' from cache.' : ' from remote data source.';
                    log('Retrieved [Health] id:' + s.id() + src, s, true);
                    if (!data.fromCache) {
                        storage.save('[Health] id:' + s.id());
                    }
                    return tagObservable(s);
                }
            }

            function refreshTag(health) {
                return EntityQuery.fromEntities(health)
                    .using(manager).execute()
                    .then(querySucceeded).fail(queryFailed);
            }

            function querySucceeded(data) {
                var s = data.results[0];
                log('Retrieved [Health] id:' + s.id() + ' from remote data source', s, true);
                storage.save('fresh [Health] w/ id:' + s.id());
                return tagObservable(s);
            }
        };
        var createTag = function(data) {
            return manager.createEntity(model.entityNames.tag, data);
        }
        var createTagMap = function(data, type) {
            return manager.createEntity(type, data);
        }
        //# END TAG

        //# OIL
        var getOilPartials = function(oilsObservable, forceRemote, where) {
            if (!forceRemote) {
                var s = getLocal('Oils', orderBy.oil, false, where);
                if (s.length > 3) {
                    oilsObservable(s);
                    return Q.resolve();
                }
            }

            var query = EntityQuery
                    .from('Oils')
                    .expand(['tags', 'tags.tag'])
                    .orderBy(orderBy.oil)
                ;


            if(where){
                query = query.where(where);
            }

            return manager.executeQuery(query)
                .then(querySucceeded)
                .fail(queryFailed);

            function querySucceeded(data) {
                var list = data.results;
                if (oilsObservable) {
                    oilsObservable(list);
                }
                storage.save(entityNames.oil);
                log('Retrieved [Oil Partials] from remote data source', list, true);
            }

        };

        //# OIL
        var getOilByTags= function(oilsObservable, forceRemote, tags) {
//            if (!forceRemote) {
//                var s = getLocal('Tags', orderBy.oil, false);
//                if (s.length > 3) {
//                    oilsObservable(s);
//                    return Q.resolve();
//                }
//            }

            var query = EntityQuery
                    .from('TagsOils')
                    .expand(['oil'])
            ;

            if (tags) {
                var criteriaPredicate = null;
                var _tags = [];
                $.each(tags(), function (index, item) {
                    if($.inArray(item.tagId(), _tags) == -1){
                        if(!criteriaPredicate){
                            criteriaPredicate = breeze.Predicate.create('tagId', '==', item.tagId());

                        }else{
                            criteriaPredicate = criteriaPredicate.or('tagId', '==', item.tagId());
                        }

                        _tags.push(item.tagId());
                    }
                });

                if(criteriaPredicate){
                    query = query.where(criteriaPredicate);
                }
            }

            return manager.executeQuery(query)
                .then(querySucceeded)
                .fail(queryFailed);

            function querySucceeded(data) {
                var list = data.results;
                if (oilsObservable) {
                    oilsObservable(list);
                }
                storage.save(entityNames.oil);
                log('Retrieved [Oil Partials] from remote data source', list, true);
            }

        };
        var getOilById = function (oilId, oilObservable) {
            var query = EntityQuery.from('Oils')
                .where('id', 'eq', oilId)
                .expand(['tags', 'tags.tag']);

            return manager.executeQuery(query).then(fetchSucceeded).fail(queryFailed);

            // 2nd - Refresh the entity from remote store (if needed)
            function fetchSucceeded(data) {
                var s = data.results[0];
                // if the entity we got is partial, we need to go get remote
                if (s.isPartial()) {
                    return refreshOil(s);
                } else {
                    var src = data.fromCache ? ' from cache.' : ' from remote data source.';
                    log('Retrieved [Oil] id:' + s.id() + src, s, true);
                    if (!data.fromCache) {
                        storage.save('[Oil] id:' + s.id());
                    }
                    return oilObservable(s);
                }
            }

            function refreshOil(oil) {
                return EntityQuery.fromEntities(oil)
                    .using(manager).execute()
                    .then(querySucceeded).fail(queryFailed);
            }

            function querySucceeded(data) {
                var s = data.results[0];
                log('Retrieved [Oil] id:' + s.id() + ' from remote data source', s, true);
                storage.save('fresh [Oil] w/ id:' + s.id());
                return oilObservable(s);
            }
        };
        var createOil = function () {
            return manager.createEntity(entityNames.oil, {isPartial: false});
        };
        //# END OIL

        //# USER
        var getUserPartials = function(usersObservable, forceRemote) {
            if (!forceRemote) {
                var s = getLocal('Users', orderBy.user);
                if (s.length > 3) {
                    usersObservable(s);
                    return Q.resolve();
                }
            }

            var query = EntityQuery
                    .from('Users')
                    .orderBy(orderBy.user)
                ;

            return manager.executeQuery(query)
                .then(querySucceeded)
                .fail(queryFailed);

            function querySucceeded(data) {
                var list = data.results;
                if (usersObservable) {
                    usersObservable(list);
                }
                storage.save(entityNames.user);
                log('Retrieved [User Partials] from remote data source', list, true);
            }

        };
        var getUserById = function (userId, userObservable) {
            var query = EntityQuery.from('Users')
                .where('id', 'eq', userId)
                .expand('tokens');

            return manager.executeQuery(query).then(fetchSucceeded).fail(queryFailed);

            // 2nd - Refresh the entity from remote store (if needed)
            function fetchSucceeded(data) {
                console.log(data)
                var s = data.results[0];
                // if the entity we got is partial, we need to go get remote
                if (s.isPartial()) {
                    return refreshUser(s);
                } else {
                    var src = data.fromCache ? ' from cache.' : ' from remote data source.';
                    log('Retrieved [User] id:' + s.id() + src, s, true);
                    if (!data.fromCache) {
                        storage.save('[User] id:' + s.id());
                    }
                    return userObservable(s);
                }
            }

            function refreshUser(user) {
                return EntityQuery.fromEntities(user)
                    .using(manager).execute()
                    .then(querySucceeded).fail(queryFailed);
            }

            function querySucceeded(data) {
                var s = data.results[0];
                log('Retrieved [User] id:' + s.id() + ' from remote data source', s, true);
                storage.save('fresh [User] w/ id:' + s.id());
                return userObservable(s);
            }
        };
        var createUser = function () {
            return manager.createEntity(entityNames.user, {isPartial: false});
        };

        var createToken = function(token) {
            token = token || {};
            return manager.createEntity('Token',
                {token: ko.unwrap(token.token), allowed: ko.unwrap(token.allowed), restricted: ko.unwrap(token.restricted), user: ko.unwrap(token.user)});
        }
        //# END USER


        var datacontext = {
            hasChanges: hasChanges,
            primeData: primeData,
            saveChanges: saveChanges,
            cancelChanges: cancelChanges,
            clearStorage: storage.clear,
            getLocal: getLocal,

            //# HEALTH
            getHealthPartials: getHealthPartials,
            getHealthById: getHealthById,
            createHealth: createHealth,
            //# /HEALTH

            // # OIL
            getOilPartials: getOilPartials,
            getOilById: getOilById,
            createOil: createOil,
            getOilByTags: getOilByTags,
            //# /OIL

            // # USER
            getUserPartials: getUserPartials,
            getUserById: getUserById,
            createUser: createUser,
            createToken: createToken,
            //# /USER

            //# /DISEASE
            createDisease: createDisease,
            createDirection: createDirection,
            //# /DISEASE

            //# TAG
            getTagById: getTagById,
            createTag: createTag,
            createTagMap: createTagMap
            //# /TAG

//            createSession: createSession,
//            getSessionPartials: getSessionPartials,
//            getSpeakerPartials: getSpeakerPartials,
//            getSessionById: getSessionById
        };
        return datacontext;

        //#region Internal methods
        function queryFailed(error) {
            var msg = 'Error retrieving data. ' + error.message;
            logError(msg, error);
            throw error;
        }

        function getLocal(resource, ordering, includeNullos, where) {
            var query = EntityQuery
                .from(resource)
                .orderBy(ordering);
            if(where){
                query = query.where(where);
            }

            if (!includeNullos) { query = query.where('id', '!=', 0); }
            return manager.executeQueryLocally(query);
        }

        function getErrorMessages(error) {
            var msg = error.message;
            if (msg.match(/validation error/i)) {
                return getValidationMessages(error);
            }
            return msg;
        }

        function getValidationMessages(error) {
            try {
                return error.entitiesWithErrors.map(function (entity) {
                    return entity.entityAspect.getValidationErrors().map(function (valError) {
                        return valError.errorMessage;
                    }).join(', <br/>');
                }).join('; <br/>');
            }
            catch (e) { /* eat it for now */ }
            return 'validation error';
        }

        function getLookups() {
            return EntityQuery.from('Lookups')
                .using(manager)
                .execute()
                .then(processLookups)
                .fail(queryFailed);
        }

        function processLookups(data) {
            model.createNullos(manager);
            storage.save('Lookups: {Tags}');
            log('Retrieved Lookup Data', data, false);
        }

        function initEntityManager(){
            var manager = new breeze.EntityManager(config.remoteServiceName);

            // Config metadataStore to add entity initializer
            model.configureMetadataStore(manager.metadataStore);

            return manager;
        }

        function log(msg, data, showToast) {
            logger.log(msg, data, system.getModuleId(datacontext), showToast);
        }

        function logError(msg, error) {
            logger.logError(msg, error, system.getModuleId(datacontext), true);
        }
});

