package main

import (
	"errors"
	"fmt"
	"path/filepath"
	"sync"
	"time"

	"github.com/aws/ec2-macos-init/internal/paths"
	"github.com/aws/ec2-macos-init/lib/ec2macosinit"
)

// run is the main runner for ec2-macOS-init.  It handles orchestration of the following major pieces:
//  1. Setup instance ID - IMDS must be up and provide an instance ID for later parts of run to work.
//  2. Read init config - Read the init.toml configuration file into the application.
//  3. Validate init config and identify modules - The config then undergoes basic validation and modules are identified.
//  4. Prioritize modules - Modules are sorted by priority into a 2D slice of modules to be run in the correct order later.
//  5. Read instance run history - The history of prior runs is read into the application for comparison of Run type settings.
//  6. Process each module by priority level - All modules are run in priority groups. Each module in a priority level
//     is started in its own goroutine and the group waits for everything in that group to finish. If any module in that
//     group fails and has FatalOnError set, the entire application exits early.
//  7. Write history file - After any run, a history.json file is written to the instance history directory for future runs.
func run(baseDir string, c *ec2macosinit.InitConfig) {

	c.Log.Info("Fetching instance ID from IMDS...")
	// An instance ID from IMDS is a prerequisite for run() to be able to check instance history
	err := SetupInstanceID(c)
	if err != nil {
		c.Log.Fatalf(computeExitCode(c, 1), "Unable to get instance ID: %s", err)
	}
	c.Log.Infof("Running on instance %s", c.IMDS.InstanceID)

	// Mark start time
	startTime := time.Now()

	// Read init config
	c.Log.Info("Reading init config...")
	err = c.ReadConfig(filepath.Join(baseDir, paths.InitTOML))
	if err != nil {
		c.Log.Fatalf(computeExitCode(c, 66), "Error while reading init config file: %s", err)
	}
	c.Log.Info("Successfully read init config")

	// Validate init config and identify modules
	c.Log.Info("Validating config...")
	err = c.ValidateAndIdentify()
	if err != nil {
		c.Log.Fatalf(computeExitCode(c, 65), "Error found during init config validation: %s", err)
	}
	c.Log.Info("Successfully validated config")

	// Prioritize modules
	c.Log.Info("Prioritizing modules...")
	err = c.PrioritizeModules()
	if err != nil {
		c.Log.Fatalf(computeExitCode(c, 1), "Error preparing and identifying modules: %s", err)
	}
	c.Log.Info("Successfully prioritized modules")

	// Create instance history directories
	c.Log.Info("Creating instance history directories for current instance...")
	err = c.CreateDirectories()
	if err != nil {
		c.Log.Fatalf(computeExitCode(c, 73), "Error creating instance history directories: %s", err)
	}
	c.Log.Info("Successfully created directories")

	// Read instance run history
	c.Log.Info("Getting instance history...")
	err = c.GetInstanceHistory()
	if err != nil {
		var herr ec2macosinit.HistoryError
		// If GetInstanceHistory() returns a HistoryError, there was invalid JSON in the history file
		// Catch this specific error to inform the user of the error and provide a way to remediate it.
		if errors.As(err, &herr) {
			c.Log.Warn("There was an error getting instance history")
			c.Log.Info("The history JSON files might be invalid and need to be restored or removed.")
			c.Log.Info("Run 'sudo ec2-macos-init clean' to remove all history files.")
		}
		c.Log.Fatalf(computeExitCode(c, 1), "Error getting instance history: %s", err)
	}
	c.Log.Info("Successfully gathered instance history")

	// Process each module by priority level
	var aggregateFatal bool
	var aggFatalModuleName string
	for i := 0; i < len(c.ModulesByPriority); i++ {
		c.Log.Infof("Processing priority level %d (%d modules)...\n", i+1, len(c.ModulesByPriority[i]))
		wg := sync.WaitGroup{}
		// Start every module within the priority level group
		for j := 0; j < len(c.ModulesByPriority[i]); j++ {
			wg.Add(1)
			go func(m *ec2macosinit.Module, h *[]ec2macosinit.History) {
				// Run module if it should be run
				if m.ShouldRun(c.IMDS.InstanceID, *h) {
					c.Log.Infof("Running module [%s] (type: %s, group: %d)\n", m.Name, m.Type, m.PriorityGroup)
					ctx := &ec2macosinit.ModuleContext{

						Logger:        c.Log,
						IMDS:          &c.IMDS,
						BaseDirectory: baseDir,
					}
					// Run appropriate module
					var message string
					var err error
					switch t := m.Type; t {
					case "command":
						message, err = m.CommandModule.Do(ctx)
					case "motd":
						message, err = m.MOTDModule.Do(ctx)
					case "sshkeys":
						message, err = m.SSHKeysModule.Do(ctx)
					case "userdata":
						message, err = m.UserDataModule.Do(ctx)
					case "networkcheck":
						message, err = m.NetworkCheckModule.Do(ctx)
					case "systemconfig":
						message, err = m.SystemConfigModule.Do(ctx)
					case "usermanagement":
						message, err = m.UserManagementModule.Do(ctx)
					default:
						message = "unknown module type"
						err = fmt.Errorf("unknown module type")
					}
					if err != nil {
						c.Log.Infof("Error while running module [%s] (type: %s, group: %d) with message: %s and err: %s\n", m.Name, m.Type, m.PriorityGroup, message, err)
						if m.FatalOnError {
							aggregateFatal = true
							aggFatalModuleName = m.Name
						}
					} else {
						// Module was successfully completed
						m.Success = true
						c.Log.Infof("Successfully completed module [%s] (type: %s, group: %d) with message: %s\n", m.Name, m.Type, m.PriorityGroup, message)
					}
				} else {
					// In the case that we choose not to run a module, it is because the module has already succeeded
					// in a prior run. For this reason, we need to pass through the success of the module to history.
					m.Success = true
					c.Log.Infof("Skipping module [%s] (type: %s, group: %d) due to Run type setting\n", m.Name, m.Type, m.PriorityGroup)
				}
				wg.Done()
			}(&c.ModulesByPriority[i][j], &c.InstanceHistory)
		}
		wg.Wait()
		c.Log.Infof("Successfully completed processing of priority level %d\n", i+1)
		// If any module failed which had FatalOnError set, trigger an aggregate fail
		if aggregateFatal {
			break
		}
	}

	// Write history file
	c.Log.Infof("Writing instance history for instance %s...", c.IMDS.InstanceID)
	err = c.WriteHistoryFile()
	if err != nil {
		c.Log.Fatalf(computeExitCode(c, 73), "Error writing instance history file: %s", err)
	}
	c.Log.Info("Successfully wrote instance history")

	// If any module triggered an aggregate fatal, exit 1
	if aggregateFatal {
		c.Log.Fatalf(computeExitCode(c, 1), "Exiting after %s due to failure in module [%s] with FatalOnError set", time.Since(startTime).String(), aggFatalModuleName)
	}

	// Log completion and total run time
	c.Log.Infof("EC2 macOS Init completed in %s", time.Since(startTime).String())
}

// computeExitCode checks to see if the number of fatal retries has been exceeded. If not, it increments the counter,
// stored in a temporary file, and returns the requested exit code. If the count is exceeded, it returns 0 to avoid
// launchd restarting forever due to the KeepAlive setting.
func computeExitCode(c *ec2macosinit.InitConfig, e int) (exitCode int) {
	// Check if other runs have happened this boot and return data about them
	exceeded, err := c.RetriesExceeded()
	if err != nil {
		c.Log.Errorf("Error while getting retry information: %s", err)
		return 1
	}

	// If the count has exceed the limit, return 0
	if exceeded {
		c.Log.Errorf("Number of fatal retries (%d) exceeded, exiting 0 to avoid infinite runs",
			c.FatalCounts.Count)
		return 0
	}

	c.Log.Infof("Fatal [%d/%d] of this boot", c.FatalCounts.Count, ec2macosinit.PerBootFatalLimit)
	// Increment the counter in the temporary file before returning
	err = c.FatalCounts.IncrementFatalCount()
	if err != nil {
		c.Log.Errorf("Unable to write fatal counts to file: %s", err)
	}

	// Return the requested exit code
	return e
}
