/** @module container */
/** @hidden */ 
let _ = require('lodash');
/** @hidden */ 
let process = require('process');

import { ConfigParams } from 'pip-services3-commons-node';
import { IReferences } from 'pip-services3-commons-node';
import { DependencyResolver } from 'pip-services3-commons-node';
import { Schema } from 'pip-services3-commons-node';
import { UnknownException } from 'pip-services3-commons-node';
import { BadRequestException } from 'pip-services3-commons-node';
import { Container } from 'pip-services3-container-node';
import { CounterTiming } from 'pip-services3-components-node';
import { ConsoleLogger } from 'pip-services3-components-node';
import { CompositeCounters } from 'pip-services3-components-node';

/**
 * Abstract AWS Lambda function, that acts as a container to instantiate and run components
 * and expose them via external entry point. 
 * 
 * When handling calls "cmd" parameter determines which what action shall be called, while
 * other parameters are passed to the action itself.
 * 
 * Container configuration for this Lambda function is stored in <code>"./config/config.yml"</code> file.
 * But this path can be overriden by <code>CONFIG_PATH</code> environment variable.
 * 
 * ### Configuration parameters ###
 * 
 * - dependencies:
 *     - controller:                  override for Controller dependency
 * - connections:                   
 *     - discovery_key:               (optional) a key to retrieve the connection from [[https://pip-services3-node.github.io/pip-services3-components-node/interfaces/connect.idiscovery.html IDiscovery]]
 *     - region:                      (optional) AWS region
 * - credentials:    
 *     - store_key:                   (optional) a key to retrieve the credentials from [[https://pip-services3-node.github.io/pip-services3-components-node/interfaces/auth.icredentialstore.html ICredentialStore]]
 *     - access_id:                   AWS access/client id
 *     - access_key:                  AWS access/client id
 * 
 * ### References ###
 * 
 * - <code>\*:logger:\*:\*:1.0</code>            (optional) [[https://pip-services3-node.github.io/pip-services3-components-node/interfaces/log.ilogger.html ILogger]] components to pass log messages
 * - <code>\*:counters:\*:\*:1.0</code>          (optional) [[https://pip-services3-node.github.io/pip-services3-components-node/interfaces/count.icounters.html ICounters]] components to pass collected measurements
 * - <code>\*:discovery:\*:\*:1.0</code>         (optional) [[https://pip-services3-node.github.io/pip-services3-components-node/interfaces/connect.idiscovery.html IDiscovery]] services to resolve connection
 * - <code>\*:credential-store:\*:\*:1.0</code>  (optional) Credential stores to resolve credentials
 * 
 * @see [[LambdaClient]]
 * 
 * ### Example ###
 * 
 *     class MyLambdaFunction extends LambdaFunction {
 *         private _controller: IMyController;
 *         ...
 *         public constructor() {
 *             base("mygroup", "MyGroup lambda function");
 *             this._dependencyResolver.put(
 *                 "controller",
 *                 new Descriptor("mygroup","controller","*","*","1.0")
 *             );
 *         }
 *      
 *         public setReferences(references: IReferences): void {
 *             base.setReferences(references);
 *             this._controller = this._dependencyResolver.getRequired<IMyController>("controller");
 *         }
 *      
 *         public register(): void {
 *             registerAction("get_mydata", null, (params, callback) => {
 *                 let correlationId = params.correlation_id;
 *                 let id = params.id;
 *                 this._controller.getMyData(correlationId, id, callback);
 *             });
 *             ...
 *         }
 *     }
 * 
 *     let lambda = new MyLambdaFunction();
 *     
 *     service.run((err) => {
 *         console.log("MyLambdaFunction is started");
 *     });
 */
export abstract class LambdaFunction extends Container {
    /**
     * The performanc counters.
     */
    protected _counters = new CompositeCounters();
    /**
     * The dependency resolver.
     */
    protected _dependencyResolver = new DependencyResolver();
    /**
     * The map of registred validation schemas.
     */
    protected _schemas: { [id: string]: Schema } = {};
    /**
     * The map of registered actions.
     */
    protected _actions: { [id: string]: any } = {};
    /**
     * The default path to config file.
     */
    protected _configPath: string = './config/config.yml';

    /**
     * Creates a new instance of this lambda function.
     * 
     * @param name          (optional) a container name (accessible via ContextInfo)
     * @param description   (optional) a container description (accessible via ContextInfo)
     */
    public constructor(name?: string, description?: string) {
        super(name, description);

        this._logger = new ConsoleLogger();
    }

    private getConfigPath(): string {
        return process.env.CONFIG_PATH || this._configPath;
    }

    private getParameters(): ConfigParams {
        let parameters = ConfigParams.fromValue(process.env);
        return parameters;
    }

    private captureErrors(correlationId: string): void {
        // Log uncaught exceptions
        process.on('uncaughtException', (ex) => {
            this._logger.fatal(correlationId, ex, "Process is terminated");
            process.exit(1);
        });
    }

    private captureExit(correlationId: string): void {
        this._logger.info(correlationId, "Press Control-C to stop the microservice...");

        // Activate graceful exit
        process.on('SIGINT', () => {
            process.exit();
        });

        // Gracefully shutdown
        process.on('exit', () => {
            this.close(correlationId);
            this._logger.info(correlationId, "Goodbye!");
        });
    }

	/**
	 * Sets references to dependent components.
	 * 
	 * @param references 	references to locate the component dependencies. 
	 */
    public setReferences(references: IReferences): void {
        super.setReferences(references);
        this._counters.setReferences(references);
        this._dependencyResolver.setReferences(references);

        this.register();
    }

    /**
     * Adds instrumentation to log calls and measure call time.
     * It returns a CounterTiming object that is used to end the time measurement.
     * 
     * @param correlationId     (optional) transaction id to trace execution through call chain.
     * @param name              a method name.
     * @returns CounterTiming object to end the time measurement.
     */
    protected instrument(correlationId: string, name: string): CounterTiming {
        this._logger.trace(correlationId, "Executing %s method", name);
        return this._counters.beginTiming(name + ".exec_time");
    }

    /**
     * Runs this lambda function, loads container configuration,
     * instantiate components and manage their lifecycle,
     * makes this function ready to access action calls.
     *  
     * @param callback callback function that receives error or null for success.
     */
    public run(callback?: (err: any) => void): void {
        let correlationId = this._info.name;

        let path = this.getConfigPath();
        let parameters = this.getParameters();
        this.readConfigFromFile(correlationId, path, parameters);

        this.captureErrors(correlationId);
        this.captureExit(correlationId);
    	this.open(correlationId, callback);
    }

    /**
     * Registers all actions in this lambda function.
     * 
     * This method is called by the service and must be overriden
     * in child classes.
     */
    protected abstract register(): void;

    /**
     * Registers an action in this lambda function.
     * 
     * @param cmd           a action/command name.
     * @param schema        a validation schema to validate received parameters.
     * @param action        an action function that is called when action is invoked.
     */
    protected registerAction(cmd: string, schema: Schema, 
        action: (params: any, callback: (err: any, result: any) => void) => void): void {
        if (cmd == '')
            throw new UnknownException(null, 'NO_COMMAND', 'Missing command');

        if (action == null)
            throw new UnknownException(null, 'NO_ACTION', 'Missing action');

        if (!_.isFunction(action))
            throw new UnknownException(null, 'ACTION_NOT_FUNCTION', 'Action is not a function');

        // Hack!!! Wrapping action to preserve prototyping context
        let actionCurl = (params, callback) => { 
            // Perform validation
            if (schema != null) {
                let correlationId = params.correlaton_id;
                let err = schema.validateAndReturnException(correlationId, params, false);
                if (err != null) {
                    callback(err, null);
                    return;
                }
            }

            // Todo: perform verification?
            action.call(this, params, callback); 
        };

        this._actions[cmd] = actionCurl;
    }

    private execute(event: any, context: any) {
        let cmd: string = event.cmd;
        let correlationId = event.correlation_id;
        
        if (cmd == null) {
            let err = new BadRequestException(
                correlationId, 
                'NO_COMMAND', 
                'Cmd parameter is missing'
            );

            context.done(err, null);
            return;
        }
        
        let action: any = this._actions[cmd];
        if (action == null) {
            let err = new BadRequestException(
                correlationId, 
                'NO_ACTION', 
                'Action ' + cmd + ' was not found'
            )
            .withDetails('command', cmd);

            context.done(err, null);
            return;
        }
        
        action(event, context.done);
    }
    
    private handler(event: any, context: any) {
        // If already started then execute
        if (this.isOpen()) {
            this.execute(event, context);
        }
        // Start before execute
        else {
            this.run((err) => {
                if (err) context.done(err, null);
                else this.execute(event, context);
            });
        }
    }
    
    /**
     * Gets entry point into this lambda function.
     * 
     * @param event     an incoming event object with invocation parameters.
     * @param context   a context object with local references.
     */
    public getHandler(): (event: any, context: any) => void {
        let self = this;
        
        // Return plugin function
        return function (event, context) {
            // Calling run with changed context
            return self.handler.call(self, event, context);
        }
    }

    /**
     * Calls registered action in this lambda function.
     * "cmd" parameter in the action parameters determin
     * what action shall be called.
     * 
     * This method shall only be used in testing.
     * 
     * @param params action parameters.
     * @param callback callback function that receives action result or error.
     */
    public act(params: any, callback: (err: any, result: any) => void): void {
        let context = {
            done: callback
        };
        this.getHandler()(params, context);
    }

}