package ec2macosinit

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"
)

// This is unused for now but will allow us to modify the version of this history in the future.
const historyVersion = 1

// History contains an instance ID, run time and a slice of individual module histories.
type History struct {
	InstanceID      string          `json:"instanceID"`
	RunTime         time.Time       `json:"runTime"`
	ModuleHistories []ModuleHistory `json:"moduleHistory"`
	Version         int             `json:"version"`
}

// ModuleHistory contains a key of the configuration struct for future comparison and whether that run was successful.
type ModuleHistory struct {
	Key     string `json:"key"`
	Success bool   `json:"success"`
}

// HistoryError wraps a normal error and gives the caller insight into the type of error.
// The caller can check the type of error and handle different types of error differently.
// Currently HistoryError only handles errors for invalid JSON but the struct is flexible
// and can be adjusted to handle several different errors differently.
type HistoryError struct {
	err error
}

func (h HistoryError) Unwrap() error {
	return h.err
}

func (h HistoryError) Error() string {
	return h.err.Error()
}

// GetInstanceHistory takes a path to instance history directory and a file name for history files and searches for
// any files that match. Then, for each file, it calls readHistoryFile() to read the file and add it to the
// InstanceHistory struct.
func (c *InitConfig) GetInstanceHistory() (err error) {
	// Read instance history directory
	dirs, err := os.ReadDir(c.HistoryPath)
	if err != nil {
		return fmt.Errorf("ec2macosinit: unable to read instance history directory: %w", err)
	}
	// For each directory, check for a history file and call readHistoryFile()
	for _, dir := range dirs {
		if dir.IsDir() {
			historyFile := filepath.Join(c.HistoryPath, dir.Name(), c.HistoryFilename)
			if info, err := os.Stat(historyFile); err == nil {
				// Check to make sure info is a file and not a directory.
				if !info.Mode().IsRegular() {
					continue
				}
				// If there is an error getting the history file or if the history file is empty do not append to Instance History
				if info.Size() == 0 {
					c.Log.Warnf("The history file exists at %s but is empty. Skipping this file...", historyFile)
					continue
				}
				history, err := readHistoryFile(historyFile)
				if err != nil {
					return fmt.Errorf("ec2macosinit: error while reading history file at %s: %w", historyFile, err)
				}
				// Append the returned History struct to the InstanceHistory slice
				c.InstanceHistory = append(c.InstanceHistory, history)
			}
		}
	}

	return nil
}

// readHistoryFile takes an instance history file and returns a History struct containing the same information.
func readHistoryFile(file string) (history History, err error) {
	// Read file
	historyBytes, err := os.ReadFile(file)
	if err != nil {
		return History{}, fmt.Errorf("ec2macosinit: error reading config file located at %s: %w", file, err)
	}

	// Unmarshal to struct
	err = json.Unmarshal(historyBytes, &history)
	if err != nil {
		return History{}, HistoryError{err: err}
	}

	return history, nil
}

// WriteHistoryFile takes ModulesByPriority and writes it to a given history path and filename as JSON.
func (c *InitConfig) WriteHistoryFile() (err error) {
	history := History{
		InstanceID: c.IMDS.InstanceID,
		RunTime:    time.Now(),
		Version:    historyVersion,
	}
	// Copy relevant fields from InitConfig to History struct
	for _, p := range c.ModulesByPriority {
		for _, m := range p {
			history.ModuleHistories = append(
				history.ModuleHistories,
				ModuleHistory{
					Key:     m.generateHistoryKey(),
					Success: m.Success,
				},
			)
		}
	}

	// Marshal to JSON
	historyBytes, err := json.Marshal(history)
	if err != nil {
		return fmt.Errorf("ec2macosinit: unable to write history file: %w", err)
	}

	// Ensure the path exists and create it if it doesn't
	err = c.CreateDirectories()
	if err != nil {
		return fmt.Errorf("ec2macosinit: unable to write history file: :%w", err)
	}

	// Write history JSON file
	path := filepath.Join(c.HistoryPath, c.IMDS.InstanceID, c.HistoryFilename)
	err = safeWrite(path, historyBytes)
	if err != nil {
		return fmt.Errorf("ec2macosinit: unable to write history file: %w", err)
	}

	return nil
}

// safeWrite writes data to the desired file path or not at all. This function
// protects against partially written or unflushed data intended for the file.
func safeWrite(path string, data []byte) error {
	f, err := os.CreateTemp(filepath.Dir(path), fmt.Sprintf(".%s.*", filepath.Base(path)))
	if err != nil {
		return err
	}
	defer os.Remove(f.Name())
	defer f.Close()

	_, err = f.Write(data)
	if err != nil {
		return err
	}

	err = f.Sync()
	if err != nil {
		return err
	}

	return os.Rename(f.Name(), path)
}

// CreateDirectories creates the instance directory, if it doesn't exist and a directory for the running instance.
func (c *InitConfig) CreateDirectories() (err error) {
	if _, err := os.Stat(filepath.Join(c.HistoryPath, c.IMDS.InstanceID)); os.IsNotExist(err) {
		err := os.MkdirAll(filepath.Join(c.HistoryPath, c.IMDS.InstanceID), 0755)
		if err != nil {
			return fmt.Errorf("ec2macosinit: unable to create directory: %w", err)
		}
	}
	return nil
}
