package hardware

import (
	"fmt"
	"math"

	tinkv1alpha1 "github.com/tinkerbell/tink/api/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	eksav1alpha1 "github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/constants"
)

// serializeHardwareSelector returns a key for use in a map unique selector.
func serializeHardwareSelector(selector eksav1alpha1.HardwareSelector) (string, error) {
	return selector.ToString()
}

// ErrDiskNotFound indicates a disk was not found for a given selector.
type ErrDiskNotFound struct {
	// A unique identifier for the selector, preferrably something useful to an end-user.
	SelectorID string
}

func (e ErrDiskNotFound) Error() string {
	return fmt.Sprintf("no disk found for hardware selector %v", e.SelectorID)
}

func (ErrDiskNotFound) Is(t error) bool {
	_, ok := t.(ErrDiskNotFound)
	return ok
}

// IndexHardware indexes Hardware instances on index by extracfting the key using fn.
func (c *Catalogue) IndexHardware(index string, fn KeyExtractorFunc) {
	c.hardwareIndex.IndexField(index, fn)
}

// InsertHardware inserts Hardware into the catalogue. If any indexes exist, the hardware is
// indexed.
func (c *Catalogue) InsertHardware(hardware *tinkv1alpha1.Hardware) error {
	if err := c.hardwareIndex.Insert(hardware); err != nil {
		return err
	}
	c.hardware = append(c.hardware, hardware)
	return nil
}

// RemoveHardwares removes a slice of hardwares from the catalogue.
func (c *Catalogue) RemoveHardwares(hardware []tinkv1alpha1.Hardware) error {
	m := make(map[string]bool, len(hardware))
	for _, hw := range hardware {
		m[getRemoveKey(hw)] = true
	}

	diff := []*tinkv1alpha1.Hardware{}
	for i, hw := range c.hardware {
		key := getRemoveKey(*hw)
		if _, ok := m[key]; !ok {
			diff = append(diff, c.hardware[i])
		} else {
			if err := c.hardwareIndex.Remove(c.hardware[i]); err != nil {
				return err
			}
		}
	}

	c.hardware = diff
	return nil
}

// getRemoveKey returns key used to search and remove hardware.
func getRemoveKey(hardware tinkv1alpha1.Hardware) string {
	return hardware.Name + ":" + hardware.Namespace
}

// AllHardware retrieves a copy of the catalogued Hardware instances.
func (c *Catalogue) AllHardware() []*tinkv1alpha1.Hardware {
	hardware := make([]*tinkv1alpha1.Hardware, len(c.hardware))
	copy(hardware, c.hardware)
	return hardware
}

// LookupHardware retrieves Hardware instances on index with a key of key. Multiple hardware _may_
// have the same key hence it can return multiple Hardware.
func (c *Catalogue) LookupHardware(index, key string) ([]*tinkv1alpha1.Hardware, error) {
	untyped, err := c.hardwareIndex.Lookup(index, key)
	if err != nil {
		return nil, err
	}

	hardware := make([]*tinkv1alpha1.Hardware, len(untyped))
	for i, v := range untyped {
		hardware[i] = v.(*tinkv1alpha1.Hardware)
	}

	return hardware, nil
}

// TotalHardware returns the total hardware registered in the catalogue.
func (c *Catalogue) TotalHardware() int {
	return len(c.hardware)
}

const HardwareIDIndex = ".Spec.Metadata.Instance.ID"

// WithHardwareIDIndex creates a Hardware index using HardwareIDIndex on .Spec.Metadata.Instance.ID
// values.
func WithHardwareIDIndex() CatalogueOption {
	return func(c *Catalogue) {
		c.IndexHardware(HardwareIDIndex, func(o interface{}) string {
			hardware := o.(*tinkv1alpha1.Hardware)
			return hardware.Spec.Metadata.Instance.ID
		})
	}
}

const HardwareBMCRefIndex = ".Spec.BmcRef"

// WithHardwareBMCRefIndex creates a Hardware index using HardwareBMCRefIndex on .Spec.BmcRef.
func WithHardwareBMCRefIndex() CatalogueOption {
	return func(c *Catalogue) {
		c.IndexHardware(HardwareBMCRefIndex, func(o interface{}) string {
			hardware := o.(*tinkv1alpha1.Hardware)
			return hardware.Spec.BMCRef.String()
		})
	}
}

// HardwareCatalogueWriter converts Machine instances to Tinkerbell Hardware and inserts them
// in a catalogue.
type HardwareCatalogueWriter struct {
	catalogue *Catalogue
}

var _ MachineWriter = &HardwareCatalogueWriter{}

// NewHardwareCatalogueWriter creates a new HardwareCatalogueWriter instance.
func NewHardwareCatalogueWriter(catalogue *Catalogue) *HardwareCatalogueWriter {
	return &HardwareCatalogueWriter{catalogue: catalogue}
}

// Write converts m to a Tinkerbell Hardware and inserts it into w's Catalogue.
func (w *HardwareCatalogueWriter) Write(m Machine) error {
	return w.catalogue.InsertHardware(hardwareFromMachine(m))
}

func hardwareFromMachine(m Machine) *tinkv1alpha1.Hardware {
	// allow is necessary to allocate memory so we can get a bool pointer required by
	// the hardware.
	allow := true

	// TODO(chrisdoherty4) Set the namespace to the CAPT namespace.
	return &tinkv1alpha1.Hardware{
		TypeMeta: newHardwareTypeMeta(),
		ObjectMeta: v1.ObjectMeta{
			Name:      m.Hostname,
			Namespace: constants.EksaSystemNamespace,
			Labels:    m.Labels,
		},
		Spec: tinkv1alpha1.HardwareSpec{
			BMCRef: newBMCRefFromMachine(m),
			Disks:  []tinkv1alpha1.Disk{{Device: m.Disk}},
			Metadata: &tinkv1alpha1.HardwareMetadata{
				Facility: &tinkv1alpha1.MetadataFacility{
					FacilityCode: "onprem",
					PlanSlug:     "c2.medium.x86",
				},
				Instance: &tinkv1alpha1.MetadataInstance{
					ID:       m.MACAddress,
					Hostname: m.Hostname,
					Ips: []*tinkv1alpha1.MetadataInstanceIP{
						{
							Address: m.IPAddress,
							Netmask: m.Netmask,
							Gateway: m.Gateway,
							Family:  4,
							Public:  true,
						},
					},
					// TODO(chrisdoherty4) Fix upstream. The OperatingSystem is used in boots to
					// detect what iPXE scripts should be served. The Kubernetes back-end nilifies
					// its response to retrieving the OS data and the handling code doesn't check
					// for nil resulting in a segfault.
					//
					// Upstream needs patching but this will suffice for now.
					OperatingSystem: &tinkv1alpha1.MetadataInstanceOperatingSystem{},
					AllowPxe:        true,
					AlwaysPxe:       true,
				},
			},
			Interfaces: []tinkv1alpha1.Interface{
				{
					Netboot: &tinkv1alpha1.Netboot{
						AllowPXE:      &allow,
						AllowWorkflow: &allow,
					},
					DHCP: &tinkv1alpha1.DHCP{
						Arch: "x86_64",
						MAC:  m.MACAddress,
						IP: &tinkv1alpha1.IP{
							Address: m.IPAddress,
							Netmask: m.Netmask,
							Gateway: m.Gateway,
							Family:  4,
						},
						// set LeaseTime to the max value so it effectively hands out max duration leases (~136 years)
						// This value gets ignored for Ubuntu because we set static IPs for it
						// It's only temporarily needed for Bottlerocket until Bottlerocket supports static IPs
						LeaseTime:   int64(math.Pow(2, 32) - 2),
						Hostname:    m.Hostname,
						NameServers: m.Nameservers,
						UEFI:        true,
						VLANID:      m.VLANID,
					},
				},
			},
		},
	}
}

// newBMCRefFromMachine returns a BMCRef pointer for Hardware.
func newBMCRefFromMachine(m Machine) *corev1.TypedLocalObjectReference {
	if m.HasBMC() {
		return &corev1.TypedLocalObjectReference{
			Name: formatBMCRef(m),
			Kind: tinkerbellBMCKind,
		}
	}

	return nil
}
