// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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 cloudwatch implements cloudwatch plugin and its configuration
package cloudwatch

import (
	"bytes"
	"encoding/json"
	"os"
	"sync"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/fileutil"
	"github.com/aws/amazon-ssm-agent/agent/jsonutil"
	"github.com/aws/amazon-ssm-agent/agent/log"
)

// ConfigFileName represents the name of the configuration file for cloud watch plugin
const (
	ConfigFileName       = "AWS.EC2.Windows.CloudWatch.json"
	ConfigFileFolderName = "awsCloudWatch"
)

var (
	instance CloudWatchConfig
	lock     sync.RWMutex
	once     sync.Once
)

type CloudWatchConfig interface {
	GetIsEnabled() bool
	Enable(engineConfiguration interface{}) error
	Disable() error
	ParseEngineConfiguration() (config string, err error)
	Update(log log.T) error
	Write() error
}

// CloudWatchConfigImpl represents the data structure of cloudwatch configuration singleton,
// which contains the essential information to configure cloudwatch plugin
type CloudWatchConfigImpl struct {
	IsEnabled           bool        `json:"IsEnabled"`
	EngineConfiguration interface{} `json:"EngineConfiguration"`
}

type EngineConfigurationParser struct {
	EngineConfiguration interface{} `json:"EngineConfiguration"`
}

// Instance returns a singleton of CloudWatchConfig instance
func Instance() CloudWatchConfig {
	once.Do(func() {
		instance = &CloudWatchConfigImpl{}
	})
	return instance
}

// ParseEngineConfiguration marshals the EngineConfiguration from interface{} to string
func (cwcInstance *CloudWatchConfigImpl) ParseEngineConfiguration() (config string, err error) {
	config, err = jsonutil.Marshal(cwcInstance.EngineConfiguration)
	return buildFullConfiguration(config), err
}

// Update updates configuration from file system
func (cwcInstance *CloudWatchConfigImpl) Update(log log.T) error {
	var cwConfig CloudWatchConfigImpl
	var err error
	if cwConfig, err = load(log); err != nil {
		return err
	}

	cwcInstance.IsEnabled = cwConfig.IsEnabled
	cwcInstance.EngineConfiguration = cwConfig.EngineConfiguration

	return err
}

// Write writes the updated configuration of cloud watch to file system
func (cwcInstance *CloudWatchConfigImpl) Write() error {
	lock.Lock()
	defer lock.Unlock()
	fileName := getFileName()
	location := getLocation()
	var err error
	var content string

	content, err = jsonutil.MarshalIndent(cwcInstance)
	if err != nil {
		return err
	}

	//verify if parent folder exist
	if !fileutil.Exists(location) {
		if err = fileutil.MakeDirs(location); err != nil {
			return err
		}
	}

	//it's fine even if we overwrite the content of previous file
	if _, err = fileutil.WriteIntoFileWithPermissions(
		fileName,
		content,
		os.FileMode(int(appconfig.ReadWriteAccess))); err != nil {
		return err
	}

	return nil
}

// Enable changes the IsEnabled property in cloud watch config from false to true
func (cwcInstance *CloudWatchConfigImpl) Enable(engineConfiguration interface{}) error {
	cwcInstance.IsEnabled = true
	cwcInstance.EngineConfiguration = engineConfiguration
	return cwcInstance.Write()
}

// Disable changes the IsEnabled property in cloud watch config from true to false
func (cwcInstance *CloudWatchConfigImpl) Disable() error {
	cwcInstance.IsEnabled = false
	return cwcInstance.Write()
}

// GetIsEnabled returns true if configuration is enabled. Otherwise, it returns false.
func (cwcInstance *CloudWatchConfigImpl) GetIsEnabled() bool {
	return cwcInstance.IsEnabled
}

// load reads cloud watch plugin configuration from config store (file system)
func load(log log.T) (CloudWatchConfigImpl, error) {
	lock.RLock()
	defer lock.RUnlock()
	fileName := getFileName()
	var err error
	var cwConfig CloudWatchConfigImpl

	err = jsonutil.UnmarshalFile(fileName, &cwConfig)

	// For backward compatibility, check if the engine configuration is read as string due to escaped characters.
	// If so, unmarshalling it again should correct the format to a tree of maps.
	switch cwConfig.EngineConfiguration.(type) {
	case string:
		log.Info("Legacy configuration was detected - Reformatting the configuration...")
		var engineConfiguration interface{}
		rawIn := json.RawMessage(cwConfig.EngineConfiguration.(string))
		json.Unmarshal([]byte(rawIn), &engineConfiguration)
		cwConfig.EngineConfiguration = engineConfiguration
	}

	// For backward compatibility, check if the engine configuration contains
	// another engine configuration parameter. If so, remove it from the map.
	switch cwConfig.EngineConfiguration.(type) {
	case map[string]interface{}:
		if ecMap, ok := cwConfig.EngineConfiguration.(map[string]interface{}); ok {
			if val, exist := ecMap["EngineConfiguration"]; exist {
				log.Info("Legacy configuration was detected - Removing a redundant parameter...")
				cwConfig.EngineConfiguration = val
			}
		}
	}

	return cwConfig, err
}

// getFileName returns the full name of the cloud watch config file.
func getFileName() string {
	return fileutil.BuildPath(appconfig.DefaultPluginPath, ConfigFileFolderName, ConfigFileName)
}

// getLocation returns the absolute path of the cloud watch config file folder.
func getLocation() string {
	return fileutil.BuildPath(appconfig.DefaultPluginPath, ConfigFileFolderName)
}

// buildFullConfiguration returns the complete cloud watch configuration for cloudwatch plugin.
func buildFullConfiguration(config string) string {
	// Put the "EngineConfiguration" back manually to pass to cloud watch exe
	var buffer bytes.Buffer
	buffer.WriteString("{\"EngineConfiguration\":")
	buffer.WriteString(config)
	buffer.WriteString("}")
	configuration := buffer.String()
	return configuration
}