// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Package installer contains terminal UI based talos interactive installer parts.
package installer

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/rivo/tview"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/talos-systems/talos/internal/pkg/tui/components"
	"github.com/talos-systems/talos/pkg/images"
	machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
	"github.com/talos-systems/talos/pkg/machinery/api/network"
	"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
	"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine"
	"github.com/talos-systems/talos/pkg/machinery/constants"
)

// cniPresets defines custom CNI presets.
var cniPresets = map[string]*machineapi.CNIConfig{
	"cilium": {
		Name: "custom",
		Urls: []string{
			"https://raw.githubusercontent.com/cilium/cilium/v1.8/install/kubernetes/quick-install.yaml",
		},
	},
}

// NewState creates new installer state.
//nolint:gocyclo
func NewState(ctx context.Context, installer *Installer, conn *Connection) (*State, error) {
	opts := &machineapi.GenerateConfigurationRequest{
		ConfigVersion: "v1alpha1",
		MachineConfig: &machineapi.MachineConfig{
			Type:              machineapi.MachineConfig_MachineType(machine.TypeInit),
			NetworkConfig:     &machineapi.NetworkConfig{},
			KubernetesVersion: constants.DefaultKubernetesVersion,
			InstallConfig: &machineapi.InstallConfig{
				InstallImage: images.DefaultInstallerImage,
			},
		},
		ClusterConfig: &machineapi.ClusterConfig{
			Name:         "talos-default",
			ControlPlane: &machineapi.ControlPlaneConfig{},
			ClusterNetwork: &machineapi.ClusterNetworkConfig{
				DnsDomain: "cluster.local",
			},
		},
	}

	if conn.ExpandingCluster() {
		opts.ClusterConfig.ControlPlane.Endpoint = fmt.Sprintf("https://%s:%d", conn.bootstrapEndpoint, constants.DefaultControlPlanePort)
	} else {
		opts.ClusterConfig.ControlPlane.Endpoint = fmt.Sprintf("https://%s:%d", conn.nodeEndpoint, constants.DefaultControlPlanePort)
	}

	installDiskOptions := []interface{}{
		components.NewTableHeaders("DEVICE NAME", "MODEL NAME", "SIZE"),
	}

	disks, err := conn.Disks()
	if err != nil {
		return nil, err
	}

	for _, msg := range disks.Messages {
		for i, disk := range msg.Disks {
			if i == 0 {
				opts.MachineConfig.InstallConfig.InstallDisk = disk.DeviceName
			}

			installDiskOptions = append(installDiskOptions, disk.DeviceName, disk.Model, humanize.Bytes(disk.Size))
		}
	}

	var machineTypes []interface{}

	if conn.ExpandingCluster() {
		machineTypes = []interface{}{
			" worker ", machineapi.MachineConfig_MachineType(machine.TypeJoin),
			" control plane ", machineapi.MachineConfig_MachineType(machine.TypeControlPlane),
		}
		opts.MachineConfig.Type = machineapi.MachineConfig_MachineType(machine.TypeControlPlane)
	} else {
		machineTypes = []interface{}{
			" control plane ", machineapi.MachineConfig_MachineType(machine.TypeInit),
		}
	}

	state := &State{
		cni:  constants.DefaultCNI,
		conn: conn,
		opts: opts,
	}

	networkConfigItems := []*components.Item{
		components.NewItem(
			"Hostname",
			v1alpha1.NetworkConfigDoc.Describe("hostname", true),
			&opts.MachineConfig.NetworkConfig.Hostname,
		),
		components.NewItem(
			"DNS Domain",
			v1alpha1.ClusterNetworkConfigDoc.Describe("dnsDomain", true),
			&opts.ClusterConfig.ClusterNetwork.DnsDomain,
		),
	}

	interfaces, err := conn.Interfaces()
	if err != nil {
		return nil, err
	}

	addedInterfaces := false
	opts.MachineConfig.NetworkConfig.Interfaces = []*machineapi.NetworkDeviceConfig{}

	for _, iface := range interfaces.Messages[0].Interfaces {
		status := ""

		if (net.Flags(iface.Flags) & net.FlagUp) != 0 {
			status = " (UP)"
		}

		if !addedInterfaces {
			networkConfigItems = append(networkConfigItems, components.NewSeparator("Network Interfaces Configuration"))
			addedInterfaces = true
		}

		networkConfigItems = append(networkConfigItems, components.NewItem(
			fmt.Sprintf("%s, %s%s", iface.Name, iface.Hardwareaddr, status),
			"",
			configureAdapter(installer, opts, iface),
		))
	}

	if !conn.ExpandingCluster() {
		networkConfigItems = append(networkConfigItems,
			components.NewSeparator(v1alpha1.ClusterNetworkConfigDoc.Describe("cni", true)),
			components.NewItem(
				"Type",
				v1alpha1.ClusterNetworkConfigDoc.Describe("cni", true),
				&state.cni,
				components.NewTableHeaders("CNI", "description"),
				constants.DefaultCNI, "CNI used by Talos by default",
				"cilium", "Cillium 1.8 installed through quick-install.yaml",
			))
	}

	state.pages = []*Page{
		NewPage("Installer Params",
			components.NewItem(
				"Image",
				v1alpha1.InstallConfigDoc.Describe("image", true),
				&opts.MachineConfig.InstallConfig.InstallImage,
			),
			components.NewSeparator(
				v1alpha1.InstallConfigDoc.Describe("disk", true),
			),
			components.NewItem(
				"Install Disk",
				"",
				&opts.MachineConfig.InstallConfig.InstallDisk,
				installDiskOptions...,
			),
		),
		NewPage("Machine Config",
			components.NewItem(
				"Machine Type",
				v1alpha1.MachineConfigDoc.Describe("type", true),
				&opts.MachineConfig.Type,
				machineTypes...,
			),
			components.NewItem(
				"Cluster Name",
				v1alpha1.ClusterConfigDoc.Describe("clusterName", true),
				&opts.ClusterConfig.Name,
			),
			components.NewItem(
				"Control Plane Endpoint",
				v1alpha1.ControlPlaneConfigDoc.Describe("endpoint", true),
				&opts.ClusterConfig.ControlPlane.Endpoint,
			),
			components.NewItem(
				"Kubernetes Version",
				"",
				&opts.MachineConfig.KubernetesVersion,
			),
			components.NewItem(
				"Allow Scheduling on Masters",
				v1alpha1.ClusterConfigDoc.Describe("allowSchedulingOnMasters", true),
				&opts.ClusterConfig.AllowSchedulingOnMasters,
			),
		),
		NewPage("Network Config",
			networkConfigItems...,
		),
	}

	return state, nil
}

// State installer state.
type State struct {
	pages []*Page
	opts  *machineapi.GenerateConfigurationRequest
	conn  *Connection
	cni   string
}

// GenConfig returns current config encoded in yaml.
func (s *State) GenConfig() (*machineapi.GenerateConfigurationResponse, error) {
	// configure custom cni from the preset
	if customCNI, ok := cniPresets[s.cni]; ok {
		s.opts.ClusterConfig.ClusterNetwork.CniConfig = customCNI
	}

	s.opts.OverrideTime = timestamppb.New(time.Now().UTC())

	return s.conn.GenerateConfiguration(s.opts)
}

func configureAdapter(installer *Installer, opts *machineapi.GenerateConfigurationRequest, adapter *network.Interface) func(item *components.Item) tview.Primitive {
	return func(item *components.Item) tview.Primitive {
		return components.NewFormModalButton(item.Name, "configure").
			SetSelectedFunc(func() {
				deviceIndex := -1
				var adapterSettings *machineapi.NetworkDeviceConfig

				for i, iface := range opts.MachineConfig.NetworkConfig.Interfaces {
					if iface.Interface == adapter.Name {
						deviceIndex = i
						adapterSettings = iface

						break
					}
				}

				if adapterSettings == nil {
					adapterSettings = &machineapi.NetworkDeviceConfig{
						Interface:   adapter.Name,
						Dhcp:        true,
						Mtu:         int32(adapter.Mtu),
						Ignore:      false,
						DhcpOptions: &machineapi.DHCPOptionsConfig{},
					}

					if len(adapter.Ipaddress) > 0 {
						adapterSettings.Cidr = adapter.Ipaddress[0]
					}
				}

				items := []*components.Item{
					components.NewItem(
						"Use DHCP",
						v1alpha1.DeviceDoc.Describe("dhcp", true),
						&adapterSettings.Dhcp,
					),
					components.NewItem(
						"Ignore",
						v1alpha1.DeviceDoc.Describe("ignore", true),
						&adapterSettings.Ignore,
					),
					components.NewItem(
						"CIDR",
						v1alpha1.DeviceDoc.Describe("cidr", true),
						&adapterSettings.Cidr,
					),
					components.NewItem(
						"MTU",
						v1alpha1.DeviceDoc.Describe("mtu", true),
						&adapterSettings.Mtu,
					),
					components.NewItem(
						"Route Metric",
						v1alpha1.DeviceDoc.Describe("dhcpOptions", true),
						&adapterSettings.DhcpOptions.RouteMetric,
					),
				}

				adapterConfiguration := components.NewForm(installer.app)
				if err := adapterConfiguration.AddFormItems(items); err != nil {
					panic(err)
				}

				focused := installer.app.GetFocus()
				page, _ := installer.pages.GetFrontPage()

				goBack := func() {
					installer.pages.SwitchToPage(page)
					installer.app.SetFocus(focused)
				}

				adapterConfiguration.AddMenuButton("Cancel", false).SetSelectedFunc(func() {
					goBack()
				})

				adapterConfiguration.AddMenuButton("Apply", false).SetSelectedFunc(func() {
					goBack()

					if deviceIndex == -1 {
						opts.MachineConfig.NetworkConfig.Interfaces = append(
							opts.MachineConfig.NetworkConfig.Interfaces,
							adapterSettings,
						)
					}
				})

				flex := tview.NewFlex().SetDirection(tview.FlexRow)
				flex.AddItem(tview.NewBox().SetBackgroundColor(color), 1, 0, false)
				flex.AddItem(adapterConfiguration, 0, 1, false)

				installer.addPage(
					fmt.Sprintf("Adapter %s Configuration", adapter.Name),
					flex,
					true,
					nil,
				)
				installer.app.SetFocus(adapterConfiguration)
			})
	}
}
