'use strict';

const Objection = require('objection');
const Model = Objection.Model;
const Knex = require('knex');

const Path = require('path');
const Hoek = require('hoek');
const Joi = require('joi');
const Schema = require('./schema');
const Package = require('../package.json');

const SchwiftyMigration = require('schwifty-migration');

const internals = {};

exports.register = function (server, options, next) {

    const rootState = internals.state(server.root);

    internals.state(server).dir = options.dir;
    const knexFile = options.knexFile;

    internals.state(server).knex = Knex(knexFile[process.env.NODE_ENV]);

    if (!rootState.setup) {

        rootState.collector = {
            models: {},
            defaults: {},
            teardownOnStop: null // Not set, effectively defaults true
        };

        // Here's the ORM!

        server.decorate('server', 'schwifty', internals.schwifty);
        server.decorate('server', 'models', internals.models((ctx) => ctx, 'realm'));
        server.decorate('request', 'models', internals.models((ctx) => ctx.server, 'route.realm'));
        server.decorate('server', 'knex', internals.knex((ctx) => ctx, 'realm'));
        server.decorate('request', 'knex', internals.knex((ctx) => ctx.server, 'route.realm'));
        server.ext('onPreStart', internals.initialize);
        server.ext('onPostStop', internals.stop);

        rootState.setup = true;
    };

    // Collect defaults

    const collector = rootState.collector;
    const defaults = knexFile.defaults || {};

    Object.keys(defaults).forEach((key) => {

        Hoek.assert(!collector.defaults[key], `Default for "${key}" has already been set.`);
        collector.defaults[key] = defaults[key];
    });

    // Decide whether server stop should teardown

    if (typeof knexFile.teardownOnStop !== 'undefined') {
        Hoek.assert(collector.teardownOnStop === null, 'Schwifty\'s teardownOnStop option can only be specified once.');
        collector.teardownOnStop = knexFile.teardownOnStop;
    }

    const config = internals.registrationConfig(knexFile);
    server.root.schwifty(config);

    next();
};

exports.register.attributes = {
    pkg: Package,
    multiple: true
};

// Massage registration config for use with rejoice
internals.registrationConfig = (options) => {

    const config = Hoek.shallow(options);

    delete config.defaults;
    delete config.teardownOnStop;

    // Resolve models

    if (typeof config.models === 'string') {
        if (Path.isAbsolute(config.models)) {
            config.models = require(config.models);
        }
        else {
            config.models = require(Path.resolve(process.cwd(), config.models));
        }
    }

    // Resolve clients

    Object.keys(config.clients || {}).forEach((name) => {

        if (typeof config.clients[name] === 'string') {
            config.clients[name] = require(config.clients[name]);
        }
    });

    return config;
};

exports.Model = class SchwiftyModel extends Model {

    constructor(...args) {
        super(...args);
    }

    // $beforeInsert() {
    //   this.createdAt = new Date().toISOString();
    // };

    // $beforeUpdate() {
    //   this.updatedAt = new Date().toISOString();
    // };

    tryValidate(schema, json) {

        return Joi.validate(json, schema);
    }

    parseValidationError(err) {

        return err;
    }

    $validate(json = this, options = {}) {

        const ModelClass = this.constructor;

        let joiSchema = ModelClass.schema;

        if (!joiSchema || options.skipValidation) {
          return json;
        }

        let validationError = null;

        let validation = this.tryValidate(joiSchema, json);

        if(validation.error) {
            throw new Objection.ValidationError(this.parseValidationError(validation.error.message));
        } else {
            validation = validation.value;
            this.$afterValidate(validation, options);
        }

        return validation;
    }
}

internals.initialize = function (server, next) {

    const collector = internals.state(server.root).collector;

    const modelsAsArray = [];

    Object.keys(collector.models).forEach((id) => {

        let model = collector.models[id];
        model = model.bindKnex(internals.state(server).knex);
        collector.models[id] = model;
        modelsAsArray.push(model);
    });

    SchwiftyMigration.initModels(modelsAsArray, internals.state(server).knex, internals.state(server).dir, (err, res) => {

        const config = {
            defaults: collector.defaults
        };

        next();
    });
};

internals.stop = function (server, next) {

    const collector = internals.state(server.root).collector;

    // Do not teardown if specifically asked not to
    if (collector.teardownOnStop === false) {
        return next();
    }

    return Knex.destroy(next);
};

internals.schwifty = function (config) {

    let isModels = false;

    // Array of models, coerce to config
    if (Array.isArray(config)) {
        config = Joi.validate(config, Schema.schwifty);
        isModels = true;
    } else {

        // Sim a Joi validation
        config = {
            error: null,
            value: config
        }
    }

    if(config.error) {
        throw new Objection.ValidationError(this.parseValidationError(config.error.message));
    } else {
        config = config.value;
    }

    if(isModels === true) {
        config = { models: config };
    }

    // Apply empty defaults
    config.models = config.models || [];

    // Collect adapters, connections, models, ensuring no dupes
    const collector = internals.state(this.root).collector;
    const modelIds = config.models.map((model) => new model().constructor.name);

    modelIds.forEach((id, index) => {

        // Classnames are often first-capital
        id = String(id).toLowerCase();
        Hoek.assert(!collector.models[id], `Model definition with identity "${id}" has already been registered.`);
        collector.models[id] = config.models[index];

        Hoek.assert(collector.models[id].tableName, `Model definition must have "static get tableName(){ return 'myTableName' }"`);
    });

    // If all went well, track which models belong to which realms
    const state = internals.state(this);
    state.models = (state.models || []).concat(modelIds);
};

internals.models = (serverFrom, realmPath) => {

    return function (all) {

        const rootCollector = internals.state(serverFrom.root).collector;

        if(!rootCollector.models) {
            return {};
        }

        if (all) {
            return rootCollector.models;
        }

        const models = Hoek.reach(this, `${realmPath}.plugins.schwifty.models`) || [];

        for (let i = 0; i < models.length; ++i) {
            const modelName = models[i].toLowerCase();
            models[modelName] = rootCollector.models[modelName];
        }

        return models;
    };
};

internals.knex = (serverFrom, realmPath) => {

    return function () {

        return Hoek.reach(this, `${realmPath}.plugins.schwifty.knex`);
    };
};

internals.state = (srv) => {

    const state = srv.realm.plugins.schwifty = srv.realm.plugins.schwifty || {};

    return state;
};

@@ -4503,32 +4503,95 @@
 err, res) =%3E %7B%0A%0A
+        if(err) %7B%0A            throw new Error(err);%0A        %7D%0A%0A
         const co

Throw error