// 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 main represents the entry point of the agent.
package main

import (
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"runtime/debug"
	"syscall"

	"github.com/aws/amazon-ssm-agent/agent/agent"
	"github.com/aws/amazon-ssm-agent/agent/agentlogstocloudwatch/cloudwatchlogspublisher"
	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/context"
	"github.com/aws/amazon-ssm-agent/agent/framework/coremanager"
	"github.com/aws/amazon-ssm-agent/agent/framework/coremodules"
	"github.com/aws/amazon-ssm-agent/agent/health"
	"github.com/aws/amazon-ssm-agent/agent/hibernation"
	"github.com/aws/amazon-ssm-agent/agent/ipc/messagebus"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/log/logger"
	"github.com/aws/amazon-ssm-agent/agent/rebooter"
	"github.com/aws/amazon-ssm-agent/agent/session/utility"
	"github.com/aws/amazon-ssm-agent/agent/ssm"
	"github.com/aws/amazon-ssm-agent/agent/startup"
	"github.com/aws/amazon-ssm-agent/agent/startup/serialport"
	"github.com/aws/amazon-ssm-agent/agent/telemetry/collector"
	dynamicConfiguration "github.com/aws/amazon-ssm-agent/agent/telemetry/dynamic_configuration"
	agentIdentity "github.com/aws/amazon-ssm-agent/common/identity"
	"github.com/aws/amazon-ssm-agent/common/identity/identity"
	"github.com/aws/amazon-ssm-agent/common/telemetry"
	telemetryConfig "github.com/aws/amazon-ssm-agent/common/telemetry/config"
	telemetryContext "github.com/aws/amazon-ssm-agent/common/telemetry/context"
)

const (
	activationCodeFlag      = "code"
	activationIDFlag        = "id"
	regionFlag              = "region"
	registerFlag            = "register"
	fingerprintFlag         = "fingerprint"
	similarityThresholdFlag = "similarityThreshold"
	workerFlag              = "worker"
)

var (
	activationCode, activationID, region     string
	register, clear, force, fpFlag, isWorker bool
	similarityThreshold                      int
	registrationFile                         = filepath.Join(appconfig.DefaultDataStorePath, "registration")
	messageBusClient                         *messagebus.MessageBus
	process                                  *startup.Processor
)

func start(log log.T, shouldCheckHibernation bool) (ssmAgent agent.ISSMAgent, err error) {
	config, err := appconfig.Config(false)
	if err != nil {
		log.Debugf("appconfig could not be loaded - %v", err)
		return nil, err
	}

	selector := identity.NewRuntimeConfigIdentitySelector(log)
	agentIdentity, err := identity.NewAgentIdentity(log, &config, selector)
	if err != nil {
		return nil, err
	}

	context := context.Default(log, config, agentIdentity, "[ssm-agent-worker]")
	log = context.Log() // get the logger again. It will have the telemetry namespace

	// initalize the telemetry SDK
	initializeTelemetry(log, agentIdentity, context)

	// Reset password for default RunAs user if already exists
	sessionUtil := &utility.SessionUtil{}
	if err := sessionUtil.ResetPasswordIfDefaultUserExists(context); err != nil {
		log.Warnf("Reset password failed, %v", err)
	}

	// Initializing the health module to send empty health pings to the service.
	healthModule := health.NewHealthCheck(context, ssm.NewService(context))
	hibernateState := hibernation.NewHibernateMode(healthModule, context)
	messageBusClient = messagebus.NewMessageBus(context)

	ssmAgent = agent.NewSSMAgent(context, healthModule, hibernateState)
	go messageBusClient.ProcessTerminationRequest()
	go messageBusClient.ProcessHealthRequest()

	// Initialize the startup module during agent start, before agent can potentially enter hibernation mode
	if !context.AppConfig().Agent.ContainerMode {
		go func() {
			process = startup.NewProcessor(context)
			processErr := process.ModuleExecute()
			if processErr != nil {
				log.Errorf("Error occurred during startup of processor: %v", processErr)
			}
		}()
	}

	if context.AppConfig().Agent.ContainerMode {
		err = startAgent(ssmAgent, context)
		return
	}

	// Do a health check before starting the agent.
	// Health check would include creating a health module and sending empty health pings to the service.
	// If response is positive, start the agent, else retry and eventually back off (hibernate/passive mode).
	if status, hibernationErr := healthModule.GetAgentState(); shouldCheckHibernation && status == health.Passive {
		// Starting hibernate mode
		go serialport.EmitSerialPortMessage(context.Log(), fmt.Sprintf("SSM Agent entering hibernation due to error: <error>%v</error>", hibernationErr.Error()))
		context.Log().Info("Entering SSM Agent hibernate - ", hibernationErr)
		go func() {
			defer func() {
				if r := recover(); r != nil {
					context.Log().Errorf("Hibernate panic: %v", r)
					context.Log().Errorf("Stacktrace:\n%s", debug.Stack())
				}
			}()
			hibernateState.ExecuteHibernation(context)
			err = startAgent(ssmAgent, context)
		}()
	} else {
		err = startAgent(ssmAgent, context)
	}
	return
}

func initializeTelemetry(log log.T, agentIdentity agentIdentity.IAgentIdentity, context context.T) {
	if !telemetryConfig.IsTelemetryEnabled(context.Log(), context.Identity(), context.AppConfig()) {
		log.Info("Telemetry is disabled")
		return
	}

	//Initialize dynamic configuration required by telemetry
	dynamicConfiguration.NewTelemetryDynamicConfiguration(log, true, filepath.Join(appconfig.DynamicConfigFolderPath, appconfig.TelemetryDynamicConfigFileName))
	telemetryCtx := telemetryContext.NewTelemetryContext(telemetry.AgentWorkerChannelName, log, agentIdentity)
	err := telemetry.Initialize(telemetryCtx)
	if err != nil {
		log.Warnf("Telemetry failed to initialize with error %v", err)
	}
	// start telemetry collector
	err = collector.StartCollection(context)
	if err != nil {
		log.Warnf("Failed to start telemetry collection with error %v", err)
	}
}

func startAgent(ssmAgent agent.ISSMAgent, context context.T) (err error) {
	cloudwatchPublisher := cloudwatchlogspublisher.NewCloudWatchPublisher(context)
	coreModules := coremodules.RegisteredCoreModules(context)
	reboot := &rebooter.SSMRebooter{}

	var cpm *coremanager.CoreManager
	if cpm, err = coremanager.NewCoreManager(context, *coreModules, cloudwatchPublisher, reboot); err != nil {
		context.Log().Errorf("error occurred when starting core manager: %v", err)
		return
	}
	ssmAgent.SetCoreManager(cpm)

	ssmAgent.Start()
	return
}

func blockUntilSignaled(log log.T) {
	// ssm-agent-worker will rely on the termination channel to be notified when to terminate
	// the agent worker will listen to OS signals until termination channel is successfully connected

	// Below channel will handle all machine initiated shutdown/reboot requests.

	// Set up channel on which to receive signal notifications.
	// We must use a buffered channel or risk missing the signal
	// if we're not ready to receive when the signal is sent.
	c := make(chan os.Signal, 1)

	// Listening for OS signals is a blocking call.
	// Only listen to signals that require us to exit.
	// Otherwise we will continue execution and exit the program.
	signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGTERM)

	log.Debug("Listening to os signal until core agent termination channel is connected")
	select {
	case s := <-c:
		log.Info("ssm-agent-worker got signal:", s, " value:", s.Signal)
		return
	case <-messageBusClient.GetTerminationChannelConnectedChan():
		log.Debug("ssm-agent-worker is connected to core agent termination channel, stopping OS signal listener")
	}

	// Clean up OS signal listener
	signal.Stop(c)
	close(c)

	log.Debug("Waiting for termination request from core agent")
	<-messageBusClient.GetTerminationRequestChan()
}

// Run as a single process. Used by Unix systems and when running agent from console.
func run(log log.T, shouldCheckHibernation bool) {
	log = log.WithContext("[ssm-agent-worker]")
	defer func() {
		// recover in case the agent panics
		// this should handle some kind of seg fault errors.
		if msg := recover(); msg != nil {
			log.Errorf("SSM Agent Worker crashed with message %v!", msg)
			log.Errorf("%s: %s", msg, debug.Stack())
		}
	}()

	log.Debugf("Current GoMaxProc value - %v", runtime.GOMAXPROCS(0))
	log.WriteEvent(logger.AgentTelemetryMessage, "", logger.AmazonAgentWorkerStartEvent)
	// run ssm agent
	agent, err := start(log, shouldCheckHibernation)
	if err != nil {
		log.Errorf("error occurred when starting ssm-agent-worker: %v", err)
		return
	}
	blockUntilSignaled(log)
	agent.Stop()
}
