// Copyright © 2020 Intel Corporation
//
// SPDX-License-Identifier: GPL-3.0-only

package model

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"gopkg.in/yaml.v2"

	"github.com/clearlinux/clr-installer/args"
	"github.com/clearlinux/clr-installer/boolset"
	"github.com/clearlinux/clr-installer/errors"
	"github.com/clearlinux/clr-installer/kernel"
	"github.com/clearlinux/clr-installer/keyboard"
	"github.com/clearlinux/clr-installer/language"
	"github.com/clearlinux/clr-installer/network"
	"github.com/clearlinux/clr-installer/storage"
	"github.com/clearlinux/clr-installer/telemetry"
	"github.com/clearlinux/clr-installer/timezone"
	"github.com/clearlinux/clr-installer/user"
	"github.com/clearlinux/clr-installer/utils"
)

const (
	// DemoVersion is hard coded string we display in log files
	// when running in demo (aka documentation mode). We will
	// now use this as a flag to not include the version in UI.
	DemoVersion = "X.Y.Z"
)

// Version of Clear Installer.
// Also used by the Makefile for releases.
// Default to the version of the program
// but may be overridden for demo/documentation mode.
// Set by Go linker in the Makefile
var Version = "undefined"

// BuildDate is set by the Go linker with the build datetime
var BuildDate = "undefined"
var testAlias = []string{}

// SystemInstall represents the system install "configuration", the target
// medias, bundles to install and whatever state a install may require
type SystemInstall struct {
	InstallSelected   map[string]storage.InstallTarget `yaml:"-"`
	TargetMedias      []*storage.BlockDevice           `yaml:"targetMedia"`
	NetworkInterfaces []*network.Interface             `yaml:"networkInterfaces,omitempty,flow"`
	Keyboard          *keyboard.Keymap                 `yaml:"keyboard,omitempty,flow"`
	Language          *language.Language               `yaml:"language,omitempty,flow"`
	Bundles           []string                         `yaml:"bundles,omitempty,flow"`
	TargetBundles     []string                         `yaml:"targetBundles,omitempty,flow"`
	UserBundles       []string                         `yaml:"userBundles,omitempty,flow"`
	Offline           bool                             `yaml:"offline,omitempty,flow"`
	HTTPSProxy        string                           `yaml:"httpsProxy,omitempty,flow"`
	Telemetry         *telemetry.Telemetry             `yaml:"telemetry,omitempty,flow"`
	Timezone          *timezone.TimeZone               `yaml:"timezone,omitempty,flow"`
	Users             []*user.User                     `yaml:"users,omitempty,flow"`
	KernelArguments   *kernel.Arguments                `yaml:"kernel-arguments,omitempty,flow"`
	Kernel            *kernel.Kernel                   `yaml:"kernel,omitempty,flow"`
	PostReboot        bool                             `yaml:"postReboot,omitempty,flow"`
	SwupdMirror       string                           `yaml:"swupdMirror,omitempty,flow"`
	AllowInsecureHTTP bool                             `yaml:"allowInsecureHTTP,omitempty,flow"`
	SwupdSkipOptional bool                             `yaml:"swupdSkipOptional,omitempty,flow"`
	PostArchive       *boolset.BoolSet                 `yaml:"postArchive,omitempty,flow"`
	Hostname          string                           `yaml:"hostname,omitempty,flow"`
	AutoUpdate        *boolset.BoolSet                 `yaml:"autoUpdate,flow"`
	TelemetryURL      string                           `yaml:"telemetryURL,omitempty,flow"`
	TelemetryTID      string                           `yaml:"telemetryTID,omitempty,flow"`
	TelemetryPolicy   string                           `yaml:"telemetryPolicy,omitempty,flow"`
	PreInstall        []*InstallHook                   `yaml:"pre-install,omitempty,flow"`
	PostInstall       []*InstallHook                   `yaml:"post-install,omitempty,flow"`
	PostImage         []*InstallHook                   `yaml:"post-image,omitempty,flow"`
	SwupdFormat       string                           `yaml:"swupdFormat,omitempty,flow"`
	Version           uint                             `yaml:"version,omitempty,flow"`
	StorageAlias      []*StorageAlias                  `yaml:"block-devices,omitempty,flow"`
	CopyNetwork       bool                             `yaml:"copyNetwork,omitempty,flow"`
	CopySwupd         bool                             `yaml:"copySwupd,omitempty,flow"`
	Environment       map[string]string                `yaml:"env,omitempty,flow"`
	CryptPass         string                           `yaml:"-"`
	MakeISO           bool                             `yaml:"iso,omitempty,flow"`
	ISOPublisher      string                           `yaml:"isoPublisher,omitempty,flow"`
	ISOApplicationID  string                           `yaml:"isoApplicationId,omitempty,flow"`
	KeepImage         bool                             `yaml:"keepImage,omitempty,flow"`
	LockFile          string                           `yaml:"-"`
	ClearCfFile       string                           `yaml:"-"`
	PreCheckDone      bool                             `yaml:"preCheckDone,omitempty,flow"`
	MediaOpts         storage.MediaOpts                `yaml:",inline"`
}

// SystemUsage is used to include additional information into the telemetry payload
type SystemUsage struct {
	InstallModel SystemInstall `yaml:",inline"`
	Hypervisor   string        `yaml:"hypervisor,omitempty,flow"`
}

// InstallHook is a commands to be executed in a given point of the install process
type InstallHook struct {
	Chroot bool   `yaml:"chroot,omitempty,flow"`
	Cmd    string `yaml:"cmd,omitempty,flow"`
}

// StorageAlias is used to expand variables in the targetMedia definitions
// a partition's block device name attribute could be declared in the form of:
//
//	Name: ${alias}p1
//
// where ${alias} was previously declared pointing to a block device file such as:
// block-devices : [
//
//	{name: "alias", file: "/dev/nvme0n1"}
//
// ]
type StorageAlias struct {
	Name       string `yaml:"name,omitempty,flow"`
	File       string `yaml:"file,omitempty,flow"`
	DeviceFile bool   `yaml:"devicefile,omitempty,flow"`
}

// InitializeDefaults ensure defaults are set such
// as initializing pointer members
func (si *SystemInstall) InitializeDefaults() {
	// Default to archiving by default
	if si.PostArchive == nil {
		si.PostArchive = boolset.NewTrue()
	} else {
		si.PostArchive.SetDefault(true)
	}

	// Default to Auto Updating enabled by default
	if si.AutoUpdate == nil {
		si.AutoUpdate = boolset.NewTrue()
	} else {
		si.AutoUpdate.SetDefault(true)
	}
}

// ClearInstallSelected clears the map of Installation Selected targets
func (si *SystemInstall) ClearInstallSelected() {
	si.InstallSelected = map[string]storage.InstallTarget{}
}

// ClearExtraKernelArguments clears all of the of custom extra kernel arguments
func (si *SystemInstall) ClearExtraKernelArguments() {
	if si.KernelArguments == nil {
		si.KernelArguments = &kernel.Arguments{}
	}

	si.KernelArguments.Add = []string{}
}

// AddExtraKernelArguments adds a set of custom extra kernel arguments to be added to the
// clr-boot-manager configuration
func (si *SystemInstall) AddExtraKernelArguments(args []string) {
	if si.KernelArguments == nil {
		si.KernelArguments = &kernel.Arguments{}
	}

	for _, curr := range args {
		if utils.StringSliceContains(si.KernelArguments.Add, curr) {
			continue
		}

		si.KernelArguments.Add = append(si.KernelArguments.Add, curr)
	}
}

// ClearRemoveKernelArguments clears all of the of custom remove kernel arguments
func (si *SystemInstall) ClearRemoveKernelArguments() {
	if si.KernelArguments == nil {
		si.KernelArguments = &kernel.Arguments{}
	}

	si.KernelArguments.Remove = []string{}
}

// RemoveKernelArguments adds a set of kernel arguments to be "black listed" on
// clear-boot-manager, meaning these arguments will never end up in the boot manager
// entry configuration
func (si *SystemInstall) RemoveKernelArguments(args []string) {
	if si.KernelArguments == nil {
		si.KernelArguments = &kernel.Arguments{}
	}

	for _, curr := range args {
		if utils.StringSliceContains(si.KernelArguments.Remove, curr) {
			continue
		}

		si.KernelArguments.Remove = append(si.KernelArguments.Remove, curr)
	}
}

// ContainsBundle returns true if the data model has a bundle and false otherwise
func (si *SystemInstall) ContainsBundle(bundle string) bool {
	for _, curr := range si.Bundles {
		if curr == bundle {
			return true
		}
	}

	return false
}

// RemoveBundle removes a bundle from the data model
func (si *SystemInstall) RemoveBundle(bundle string) {
	bundles := []string{}

	for _, curr := range si.Bundles {
		if curr != bundle {
			bundles = append(bundles, curr)
		}
	}

	si.Bundles = bundles
}

// AddBundle adds a new bundle to the data model, we make sure to not duplicate entries
func (si *SystemInstall) AddBundle(bundle string) {
	for _, curr := range si.Bundles {
		if curr == bundle {
			return
		}
	}

	si.Bundles = append(si.Bundles, bundle)
}

// ContainsUserBundle returns true if the data model has a user bundle and false otherwise
func (si *SystemInstall) ContainsUserBundle(bundle string) bool {
	for _, curr := range si.UserBundles {
		if curr == bundle {
			return true
		}
	}

	return false
}

// RemoveUserBundle removes a user bundle from the data model
func (si *SystemInstall) RemoveUserBundle(bundle string) {
	bundles := []string{}

	for _, curr := range si.UserBundles {
		if curr != bundle {
			bundles = append(bundles, curr)
		}
	}

	si.UserBundles = bundles
}

// AddUserBundle adds a new user bundle to the data model, we make sure to not duplicate entries
func (si *SystemInstall) AddUserBundle(bundle string) {
	for _, curr := range si.UserBundles {
		if curr == bundle {
			return
		}
	}

	si.UserBundles = append([]string{bundle}, si.UserBundles...)
}

// OverrideBundles replaces the current bundles with the override list
// Sets the kernel to one of the kernel bundles, the rest remain in the list
func (si *SystemInstall) OverrideBundles(overrideBundles []string) {
	var kernelBundles []string
	si.Bundles = []string{} // Clear any existing bundles

	for _, bundle := range overrideBundles {
		if strings.HasPrefix(bundle, "kernel-") {
			kernelBundles = append(kernelBundles, bundle)
			continue
		}
		si.Bundles = append(si.Bundles, bundle) // Set Bundles
	}

	// Sort the kernels as we want one that had the CDROM loadable
	// kernel modules by default: native or LTS
	sort.Sort(sort.Reverse(sort.StringSlice(kernelBundles)))

	var foundKernel bool
	for _, bundle := range kernelBundles {
		// Only use the first kernel bundle
		if !foundKernel {
			foundKernel = true
			si.Kernel = &kernel.Kernel{Bundle: bundle}
		} else {
			si.Bundles = append(si.Bundles, bundle) // Add extra kernel bundles
			fmt.Printf("WARNING: Extra kernel bundle '%s' detected; '%s' already in use.\n",
				bundle, si.Kernel.Bundle)
		}
	}
}

// IsTargetDesktopInstall determines if this installation is a Desktop
// installation by check all bundle lists for any desktop bundles.
func (si *SystemInstall) IsTargetDesktopInstall() bool {
	isDesktop := false

	// Check the default bundle list
	for _, curr := range si.Bundles {
		if strings.Contains(strings.ToLower(curr), "desktop") {
			isDesktop = true
			break
		}
	}

	if !isDesktop {
		// Check the user bundle list
		for _, curr := range si.UserBundles {
			if strings.Contains(strings.ToLower(curr), "desktop") {
				isDesktop = true
				break
			}
		}
	}

	return isDesktop
}

// RemoveAllUsers remove from the data model all previously added user
func (si *SystemInstall) RemoveAllUsers() {
	si.Users = []*user.User{}
}

// AddUser adds a new user to the data model, this function also prevents duplicate entries
func (si *SystemInstall) AddUser(usr *user.User) {
	for _, curr := range si.Users {
		if curr.Equals(usr) {
			return
		}
	}

	si.Users = append(si.Users, usr)
}

// EncryptionRequiresPassphrase checks all partition to see if encryption was enabled
func (si *SystemInstall) EncryptionRequiresPassphrase(isAdvanced bool) bool {
	enabled := false

	for _, curr := range si.TargetMedias {
		enabled = enabled || curr.EncryptionRequiresPassphrase(isAdvanced)
	}

	return enabled
}

// Validate checks the model for possible inconsistencies or "minimum required"
// information
func (si *SystemInstall) Validate() error {
	// si will be nil if we fail to unmarshall (coverage tests has a case for that)
	if si == nil {
		return errors.ValidationErrorf("model is nil")
	}

	if si.TargetMedias == nil || len(si.TargetMedias) == 0 {
		return errors.ValidationErrorf("System Installation must provide a target media")
	}

	var results []string
	if si.IsTargetDesktopInstall() {
		results = storage.DesktopValidatePartitions(si.TargetMedias, si.MediaOpts)
	} else {
		results = storage.ServerValidatePartitions(si.TargetMedias, si.MediaOpts)
	}
	if len(results) > 0 && !si.MediaOpts.SkipValidationAll {
		return errors.ValidationErrorf(strings.Join(results, ", "))
	}

	if si.Timezone == nil {
		return errors.ValidationErrorf("Timezone not set")
	}

	if si.Keyboard == nil {
		return errors.ValidationErrorf("Keyboard not set")
	}

	if si.Language == nil {
		return errors.ValidationErrorf("System Language not set")
	}

	if si.Telemetry == nil {
		return errors.ValidationErrorf("Telemetry not acknowledged")
	}

	if si.Kernel == nil {
		return errors.ValidationErrorf("A kernel must be provided")
	}

	if len(si.ISOPublisher) > 128 {
		return errors.ValidationErrorf("isoPublisher must be shorter than 128 characters")
	}

	if len(si.ISOApplicationID) > 128 {
		return errors.ValidationErrorf("isoApplicationId must be shorter than 128 characters")
	}

	return nil
}

// AddTargetMedia adds a BlockDevice instance to the list of TargetMedias
// if bd was previously added to as a target media its pointer is updated
func (si *SystemInstall) AddTargetMedia(bd *storage.BlockDevice) {
	if si.TargetMedias == nil {
		si.TargetMedias = []*storage.BlockDevice{}
	}

	nList := []*storage.BlockDevice{bd}

	for _, curr := range si.TargetMedias {
		if !bd.Equals(curr) {
			nList = append(nList, curr)
		}
	}

	si.TargetMedias = nList
}

// AddNetworkInterface adds an Interface instance to the list of NetworkInterfaces
func (si *SystemInstall) AddNetworkInterface(iface *network.Interface) {
	if si.NetworkInterfaces == nil {
		si.NetworkInterfaces = []*network.Interface{}
	}

	si.NetworkInterfaces = append(si.NetworkInterfaces, iface)
}

// LoadFile loads a model from a yaml file pointed by path
func LoadFile(path string, options args.Args) (*SystemInstall, error) {
	var result SystemInstall

	if _, err := os.Stat(path); err == nil {
		configStr, err := ioutil.ReadFile(path)
		if err != nil {
			return nil, errors.Wrap(err)
		}

		err = yaml.UnmarshalStrict(configStr, &result)
		if err != nil {
			return nil, errors.Wrap(err)
		}
	}

	result.InitializeDefaults()

	// Set default Timezone if not defined
	if result.Timezone == nil {
		result.Timezone = &timezone.TimeZone{Code: timezone.DefaultTimezone}
	}

	// Set default Keyboard if not defined
	if result.Keyboard == nil {
		result.Keyboard = &keyboard.Keymap{Code: keyboard.DefaultKeyboard}
	}

	// Set default Language if not defined
	if result.Language == nil {
		result.Language = &language.Language{Code: language.DefaultLanguage}
	}

	// Running in VirtualBox force the default to 'kernel-lts' if
	// we are using the system default configuration file
	// See https://github.com/clearlinux/clr-installer/issues/203
	if options.ConfigFile == "" && utils.IsVirtualBox() {
		result.Kernel = &kernel.Kernel{Bundle: "kernel-lts"}
	}

	tmp := map[string]*StorageAlias{}

	for _, bds := range result.StorageAlias {
		tmp[bds.Name] = bds
	}

	for _, bds := range options.BlockDevices {
		var tks []string

		if tks = strings.Split(bds, ":"); len(tks) < 2 {
			continue
		}

		tmp[tks[0]] = &StorageAlias{Name: tks[0], File: tks[1]}
	}

	result.StorageAlias = []*StorageAlias{}

	for _, bds := range tmp {
		result.StorageAlias = append(result.StorageAlias, bds)
	}

	if len(result.StorageAlias) > 0 {
		alias := map[string]string{}
		keepMe := []*StorageAlias{}

		for _, curr := range result.StorageAlias {
			if !isAliasInUse(result.TargetMedias, curr) {
				continue
			}

			fi, err := os.Lstat(curr.File)
			inTestAlias := isTestAlias(curr.File)

			// could be an image file to be created so we fail only if the error doesn't
			// indicate the image file doesn't exist
			if err != nil && !inTestAlias && !os.IsNotExist(err) {
				return nil, errors.Wrap(err)
			}

			keepMe = append(keepMe, curr)

			if !inTestAlias && os.IsNotExist(err) {
				continue
			}

			if (fi != nil && fi.Mode()&os.ModeDevice == 0) && !inTestAlias {
				continue
			}

			curr.DeviceFile = true
			alias[curr.Name] = filepath.Base(curr.File)
		}

		// keep only the aliases we're using
		result.StorageAlias = keepMe

		for _, bd := range result.TargetMedias {
			bd.ExpandName(alias)
		}
	}

	if result.MediaOpts.SwapFileSize != "" {
		result.MediaOpts.SwapFileSet = true
	}

	return &result, nil
}

func isAliasInUse(bds []*storage.BlockDevice, alias *StorageAlias) bool {
	for _, curr := range bds {
		rep := fmt.Sprintf("${%s}", alias.Name)

		if strings.Contains(curr.Name, rep) {
			return true
		}

		if isAliasInUse(curr.Children, alias) {
			return true
		}
	}

	return false
}

func isTestAlias(file string) bool {
	if len(testAlias) == 0 {
		return false
	}

	return utils.StringSliceContains(testAlias, file)
}

// EnableTelemetry operates on the telemetry flag and enables or disables the target
// systems telemetry support based in enable argument
func (si *SystemInstall) EnableTelemetry(enable bool) {
	if si.Telemetry == nil {
		si.Telemetry = &telemetry.Telemetry{}
	}

	si.Telemetry.SetEnable(enable)
}

// IsTelemetryEnabled returns true if telemetry is enabled, false otherwise
func (si *SystemInstall) IsTelemetryEnabled() bool {
	if si.Telemetry == nil {
		return false
	}

	return si.Telemetry.Enabled
}

// IsTelemetryInstalled return true if telemetry tooling is present, false otherwise
func (si *SystemInstall) IsTelemetryInstalled() bool {
	return si.Telemetry.Installed("")
}

// WriteFile writes a yaml formatted representation of si into the provided file path
func (si *SystemInstall) WriteFile(path string) error {
	// Sanitized the model to item which should never be written
	var copyModel SystemInstall

	// Marshal current into bytes
	confBytes, bytesErr := yaml.Marshal(si)
	if bytesErr != nil {
		return errors.Wrap(bytesErr)
	}

	// Unmarshal into a copy
	if yamlErr := yaml.UnmarshalStrict(confBytes, &copyModel); yamlErr != nil {
		return errors.Wrap(bytesErr)
	}

	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		return err
	}

	defer func() {
		_ = f.Close()
	}()

	// Screen item we never want stored in a YAML
	// SkipValidation flags are okay for ready, but we
	// never want to store them -- force use to set them
	// Setting to default means they are omitted
	copyModel.MediaOpts.SkipValidationAll = false
	copyModel.MediaOpts.SkipValidationSize = false

	b, err := yaml.Marshal(copyModel)
	if err != nil {
		return err
	}

	// Write our header
	_, err = f.WriteString("#clear-linux-config\n")
	if err != nil {
		return err
	}
	// Write our version
	_, err = f.WriteString("#generated by clr-installer:" + Version + "\n")
	if err != nil {
		return err
	}
	// Write datetime stamp
	t := time.Now().UTC()
	_, err = f.WriteString("#generated on: " + fmt.Sprintf("%d-%02d-%02d_%02d:%02d:%02d_UTC\n",
		t.Year(), t.Month(), t.Day(),
		t.Hour(), t.Minute(), t.Second()))
	if err != nil {
		return err
	}

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

	return nil
}

// WriteScrubModelTargetMedias writes out a copy the model with the
// TargetMedias removed to a temporary file
func (si *SystemInstall) WriteScrubModelTargetMedias() (string, error) {
	// Sanitized the model to remove media
	var cleanModel SystemInstall

	// Marshal current into bytes
	confBytes, bytesErr := yaml.Marshal(si)
	if bytesErr != nil {
		return "", errors.Wrap(bytesErr)
	}

	// Unmarshal into a copy
	if yamlErr := yaml.UnmarshalStrict(confBytes, &cleanModel); yamlErr != nil {
		return "", errors.Wrap(bytesErr)
	}

	// Sanitize the config data to remove any potential
	// Remove the target media
	cleanModel.TargetMedias = nil

	// Remove the Media swapfile if set as we might return with a swap partition
	cleanModel.MediaOpts.SwapFileSize = ""

	tmpYaml, err := ioutil.TempFile("", "clr-installer-noMedia-*.yaml")
	if err != nil {
		return "", errors.Errorf("Could not make YAML tempfile: %v", err)
	}

	if saveErr := cleanModel.WriteFile(tmpYaml.Name()); saveErr != nil {
		return "", errors.Errorf("Could not save config to %s", tmpYaml.Name())
	}

	return tmpYaml.Name(), nil
}

// InteractiveOptionsValid ensures that options which are not appropriate
// for interactive runs are screened
func (si *SystemInstall) InteractiveOptionsValid() error {
	if si.Offline {
		return fmt.Errorf("Incompatible flag '--offline' for the interactive installer")
	}
	if si.MakeISO {
		return fmt.Errorf("Incompatible flag '--iso' for the interactive installer")
	}

	return nil
}

// SetDefaultSwapFileSize defines the swapfile sized based on
// the storage default swapfile size
func (si *SystemInstall) SetDefaultSwapFileSize() {
	if si.MediaOpts.SwapFileSize == "" {
		si.MediaOpts.SwapFileSize, _ = storage.HumanReadableSizeXiBWithPrecision(storage.SwapFileSizeDefault, 1)
	}
}

// ResetDefaultSwapFileSize clears the swapfile size unless it
// has been explicitly set by the YAML or command line
func (si *SystemInstall) ResetDefaultSwapFileSize() {
	if !si.MediaOpts.SwapFileSet {
		si.MediaOpts.SwapFileSize = ""
	}
}
