/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package deployers

import (
	"github.com/apache/openwhisk-client-go/whisk"
	"github.com/apache/openwhisk-wskdeploy/parsers"
	"github.com/apache/openwhisk-wskdeploy/utils"
	"github.com/apache/openwhisk-wskdeploy/wskenv"
	"github.com/apache/openwhisk-wskdeploy/wski18n"
	"github.com/apache/openwhisk-wskdeploy/wskprint"
)

type DeploymentReader struct {
	serviceDeployer      *ServiceDeployer
	DeploymentDescriptor *parsers.YAML
}

func NewDeploymentReader(serviceDeployer *ServiceDeployer) *DeploymentReader {
	var dep DeploymentReader
	dep.serviceDeployer = serviceDeployer

	return &dep
}

// Wrapper parser to handle yaml dir
func (reader *DeploymentReader) HandleYaml() error {

	dep := reader.serviceDeployer

	deploymentParser := parsers.NewYAMLParser()
	deployment, err := deploymentParser.ParseDeployment(dep.DeploymentPath)
	reader.DeploymentDescriptor = deployment

	return err
}

// Update entities with deployment settings
func (reader *DeploymentReader) BindAssets() error {

	var paramsCLI interface{}
	var err error

	// check if any inputs/parameters are specified in CLI using --param or --param-file
	// store params in Key/value pairs
	if len(utils.Flags.Param) > 0 {
		paramsCLI, err = utils.GetJSONFromStrings(utils.Flags.Param, false)
		if err != nil {
			return err
		}
	}

	if err := reader.bindPackageInputsAndAnnotations(paramsCLI); err != nil {
		return err
	}
	if err := reader.bindActionInputsAndAnnotations(paramsCLI); err != nil {
		return err
	}
	if err := reader.bindTriggerInputsAndAnnotations(paramsCLI); err != nil {
		return err
	}

	return nil
}

func (reader *DeploymentReader) getPackageMap() map[string]parsers.Package {
	packMap := make(map[string]parsers.Package)

	// Create local packages list from Deployment file for us to iterate over
	// either from top-level or under project schema
	if len(reader.DeploymentDescriptor.GetProject().Packages) == 0 {

		if len(reader.DeploymentDescriptor.Packages) > 0 {
			infoMsg := wski18n.T(
				wski18n.ID_DEBUG_PACKAGES_FOUND_UNDER_ROOT_X_path_X,
				map[string]interface{}{
					wski18n.KEY_PATH: reader.DeploymentDescriptor.Filepath})
			wskprint.PrintlnOpenWhiskTrace(utils.Flags.Trace, infoMsg)
			for packName, depPacks := range reader.DeploymentDescriptor.Packages {
				depPacks.Packagename = packName
				packMap[packName] = depPacks
			}
		}
	} else {

		infoMsg := wski18n.T(
			wski18n.ID_DEBUG_PACKAGES_FOUND_UNDER_PROJECT_X_path_X_name_X,
			map[string]interface{}{
				wski18n.KEY_PATH: reader.DeploymentDescriptor.Filepath,
				wski18n.KEY_NAME: reader.DeploymentDescriptor.GetProject().Name})
		wskprint.PrintlnOpenWhiskTrace(utils.Flags.Trace, infoMsg)

		for packName, depPacks := range reader.DeploymentDescriptor.GetProject().Packages {
			depPacks.Packagename = packName
			packMap[packName] = depPacks
		}
	}

	return packMap
}

func (reader *DeploymentReader) getListOfParameters(inputs map[string]parsers.Parameter) whisk.KeyValueArr {
	keyValArr := make(whisk.KeyValueArr, 0)
	for name, input := range inputs {
		var keyVal whisk.KeyValue
		keyVal.Key = name
		keyVal.Value = wskenv.InterpolateStringWithEnvVar(input.Value)
		keyValArr = append(keyValArr, keyVal)
	}
	return keyValArr
}

func (reader *DeploymentReader) getListOfAnnotations(inputs map[string]interface{}) whisk.KeyValueArr {
	keyValArr := make(whisk.KeyValueArr, 0)
	for name, input := range inputs {
		var keyVal whisk.KeyValue
		keyVal.Key = name
		keyVal.Value = wskenv.InterpolateStringWithEnvVar(input)
		keyValArr = append(keyValArr, keyVal)
	}
	return keyValArr
}

func (reader *DeploymentReader) bindPackageInputsAndAnnotations(paramsCLI interface{}) error {

	// retrieve "packages" list from depl. file; either at top-level or under "Project" schema
	packMap := reader.getPackageMap()

	for packName, pack := range packMap {

		serviceDeployPack := reader.serviceDeployer.Deployment.Packages[packName]

		if serviceDeployPack == nil {
			displayEntityNotFoundInDeploymentWarning(parsers.YAML_KEY_PACKAGE, packName)
			break
		}

		displayEntityFoundInDeploymentTrace(parsers.YAML_KEY_PACKAGE, packName)

		if len(pack.Inputs) > 0 {

			keyValArr := reader.getListOfParameters(pack.Inputs)

			depParams := make(map[string]whisk.KeyValue)
			for _, kv := range keyValArr {
				depParams[kv.Key] = kv
			}

			for _, keyVal := range serviceDeployPack.Package.Parameters {
				if _, exists := depParams[keyVal.Key]; !exists {
					keyValArr = append(keyValArr, keyVal)
				}
			}

			packageInputs := make(whisk.KeyValueArr, 0)

			if paramsCLI != nil {
				for _, kv := range keyValArr {
					// check if this particular input is specified on CLI
					if v, ok := paramsCLI.(map[string]interface{})[kv.Key]; ok {
						kv.Value = wskenv.ConvertSingleName(v.(string))
					}
					packageInputs = append(packageInputs, kv)
				}
			} else {
				packageInputs = keyValArr
			}

			serviceDeployPack.Package.Parameters = packageInputs
		}

		if len(pack.Annotations) > 0 {

			keyValArr := reader.getListOfAnnotations(pack.Annotations)

			// iterate over each annotation from deployment file
			for _, keyVal := range serviceDeployPack.Package.Annotations {
				if _, exists := pack.Annotations[keyVal.Key]; !exists {
					keyValArr = append(keyValArr, keyVal)
				}
			}

			serviceDeployPack.Package.Annotations = keyValArr
		}
	}
	return nil
}

func (reader *DeploymentReader) bindActionInputsAndAnnotations(paramsCLI interface{}) error {

	// retrieve "packages" list from depl. file; either at top-level or under "Project" schema
	packMap := reader.getPackageMap()

	for packName, pack := range packMap {

		serviceDeployPack := reader.serviceDeployer.Deployment.Packages[packName]

		if serviceDeployPack == nil {
			displayEntityNotFoundInDeploymentWarning(parsers.YAML_KEY_PACKAGE, packName)
			break
		}

		for actionName, action := range pack.Actions {

			keyValArr := make(whisk.KeyValueArr, 0)

			if len(action.Inputs) > 0 {
				keyValArr = reader.getListOfParameters(action.Inputs)

				if wskAction, exists := serviceDeployPack.Actions[actionName]; exists {

					displayEntityFoundInDeploymentTrace(parsers.YAML_KEY_ACTION, actionName)

					depParams := make(map[string]whisk.KeyValue)
					for _, kv := range keyValArr {
						depParams[kv.Key] = kv
					}

					for _, keyVal := range wskAction.Action.Parameters {
						if _, exists := depParams[keyVal.Key]; !exists {
							keyValArr = append(keyValArr, keyVal)
						}
					}

					actionInputs := make(whisk.KeyValueArr, 0)

					if paramsCLI != nil {
						for _, kv := range keyValArr {
							// check if this particular input is specified on CLI
							if v, ok := paramsCLI.(map[string]interface{})[kv.Key]; ok {
								kv.Value = wskenv.ConvertSingleName(v.(string))
							}
							actionInputs = append(actionInputs, kv)
						}
					} else {
						actionInputs = keyValArr
					}

					wskAction.Action.Parameters = actionInputs
				} else {
					displayEntityNotFoundInDeploymentWarning(parsers.YAML_KEY_ACTION, actionName)
				}
			}

			if len(action.Annotations) > 0 {

				keyValArr = reader.getListOfAnnotations(action.Annotations)

				if wskAction, exists := serviceDeployPack.Actions[actionName]; exists {

					displayEntityFoundInDeploymentTrace(parsers.YAML_KEY_ACTION, actionName)

					for _, keyVal := range wskAction.Action.Annotations {
						if _, exists := action.Annotations[keyVal.Key]; !exists {
							keyValArr = append(keyValArr, keyVal)
						}
					}
					wskAction.Action.Annotations = keyValArr
				} else {
					displayEntityNotFoundInDeploymentWarning(parsers.YAML_KEY_ACTION, actionName)
				}
			}
		}
	}
	return nil
}

func (reader *DeploymentReader) bindTriggerInputsAndAnnotations(paramsCLI interface{}) error {

	// retrieve "packages" list from depl. file; either at top-level or under "Project" schema
	packMap := reader.getPackageMap()

	// go through all packages in our local package map
	for _, pack := range packMap {
		serviceDeployment := reader.serviceDeployer.Deployment

		// for each Deployment file Trigger found in the current package
		for triggerName, trigger := range pack.Triggers {

			// If the Deployment file trigger has Input values we will attempt to bind them
			if len(trigger.Inputs) > 0 {
				keyValArr := reader.getListOfParameters(trigger.Inputs)

				// See if a matching Trigger (name) exists in manifest
				if wskTrigger, exists := serviceDeployment.Triggers[triggerName]; exists {

					displayEntityFoundInDeploymentTrace(parsers.YAML_KEY_TRIGGER, triggerName)

					depParams := make(map[string]whisk.KeyValue)
					for _, kv := range keyValArr {
						depParams[kv.Key] = kv
					}

					for _, keyVal := range wskTrigger.Parameters {
						// TODO() verify logic and add Verbose/trace say "found" or "not found"
						if _, exists := depParams[keyVal.Key]; !exists {
							displayEntityFoundInDeploymentTrace(
								parsers.YAML_KEY_ANNOTATION, keyVal.Key)
							keyValArr = append(keyValArr, keyVal)
						}
					}

					triggerInputs := make(whisk.KeyValueArr, 0)

					if paramsCLI != nil {
						for _, kv := range keyValArr {
							// check if this particular input is specified on CLI
							if v, ok := paramsCLI.(map[string]interface{})[kv.Key]; ok {
								kv.Value = wskenv.ConvertSingleName(v.(string))
							}
							triggerInputs = append(triggerInputs, kv)
						}
					} else {
						triggerInputs = keyValArr
					}

					wskTrigger.Parameters = triggerInputs
				} else {
					displayEntityNotFoundInDeploymentWarning(parsers.YAML_KEY_TRIGGER, triggerName)
				}
			}

			if len(trigger.Annotations) > 0 {

				keyValArr := reader.getListOfAnnotations(trigger.Annotations)

				if wskTrigger, exists := serviceDeployment.Triggers[triggerName]; exists {

					displayEntityFoundInDeploymentTrace(parsers.YAML_KEY_TRIGGER, triggerName)

					for _, keyVal := range wskTrigger.Annotations {
						if _, exists := trigger.Annotations[keyVal.Key]; !exists {
							keyValArr = append(keyValArr, keyVal)
						}
					}
					wskTrigger.Annotations = keyValArr
				} else {
					displayEntityNotFoundInDeploymentWarning(parsers.YAML_KEY_TRIGGER, triggerName)
				}
			}

		}

	}
	return nil
}

func displayEntityNotFoundInDeploymentWarning(entityType string, entityName string) {
	warnMsg := wski18n.T(
		wski18n.ID_WARN_DEPLOYMENT_NAME_NOT_FOUND_X_key_X_name_X,
		map[string]interface{}{
			wski18n.KEY_KEY:  entityType,
			wski18n.KEY_NAME: entityName})
	wskprint.PrintOpenWhiskWarning(warnMsg)
}

func displayEntityFoundInDeploymentTrace(entityType string, entityName string) {
	infoMsg := wski18n.T(
		wski18n.ID_DEBUG_DEPLOYMENT_NAME_FOUND_X_key_X_name_X,
		map[string]interface{}{
			wski18n.KEY_KEY:  entityType,
			wski18n.KEY_NAME: entityName})
	wskprint.PrintlnOpenWhiskTrace(utils.Flags.Trace, infoMsg)
}
