// Copyright 2024 WIT.COM Inc Licensed GPL 3.0

package main

import (
	"encoding/xml"
	"errors"
	"fmt"

	"github.com/google/uuid"
	pb "go.wit.com/lib/protobuf/virtbuf"
	"go.wit.com/log"
	"libvirt.org/go/libvirtxml"
)

// import a libvirt xml file
func addDomainDroplet(domcfg *libvirtxml.Domain) (*DropletT, []*pb.Event, error) {
	var alle []*pb.Event
	if domcfg == nil {
		return nil, alle, errors.New("domcfg == nil")
	}

	d, err := findDomain(domcfg)
	if err != nil {
		return nil, alle, err
	}
	if d == nil {
		// this is a new unknown droplet (not in the config file)
		d = new(DropletT)

		d.pb = me.cluster.AddDroplet(domcfg.UUID, domcfg.Name, 2, 2*1024*1024)
		d.pb.StartState = pb.DropletState_OFF
		d.CurrentState = pb.DropletState_UNKNOWN

		// if the domcfg doesn't have a uuid, make a new one here
		if d.pb.Uuid == "" {
			u := uuid.New()
			d.pb.Uuid = u.String()
		}

		me.droplets = append(me.droplets, d)
		me.changed = true
	}

	alle, err = updateDroplet(d, domcfg)
	if err != nil {
		log.Info("updateDroplet() failed for", d.pb.Hostname)
		return d, alle, errors.New("update failed for " + domcfg.Name)
	}
	log.Info("added new droplet", domcfg.Name, domcfg.UUID)
	dumpNonStandardXML(domcfg)
	return d, alle, nil
}

func findDomain(domcfg *libvirtxml.Domain) (*DropletT, error) {
	var found *DropletT
	if domcfg == nil {
		return nil, errors.New("domcfg == nil")
	}

	for _, d := range me.droplets {
		if d.pb.Hostname == domcfg.Name {
			if d.pb.Uuid != domcfg.UUID {
				if domcfg.UUID == "" {
					// ignore blank or nonexistent UUID's
					// todo: check to see if the uuid already exists ?
					domcfg.UUID = d.pb.Uuid
				} else {
					fmt.Println("Will Change UUID from", d.pb.Uuid, "to", domcfg.UUID, "for hostname", d.pb.Hostname)
					d.pb.Uuid = domcfg.UUID
					me.changed = true
				}
			}
			if found == nil {
				found = d
			} else {
				fmt.Println("FOUND TWICE", d.pb.Uuid, domcfg.Name, domcfg.UUID)
				return d, errors.New("Found Twice")
			}

		}
		if d.pb.Uuid == domcfg.UUID {
			if d.pb.Hostname != domcfg.Name {
				fmt.Println("protobuf has: UUID and Name:", d.pb.Uuid, d.pb.Hostname)
				fmt.Println("libvirt  has: UUID and Name:", domcfg.UUID, domcfg.Name)
				fmt.Println("FOUND UUID WITH MIS-MATCHED NAME", domcfg.Name, domcfg.UUID)
				return d, errors.New("UUID with mis-matched names")
			}
		}
	}

	return found, nil
}

func updateDroplet(d *DropletT, domcfg *libvirtxml.Domain) ([]*pb.Event, error) {
	var alle []*pb.Event

	if d == nil {
		return alle, errors.New("d == nil")
	}
	if domcfg == nil {
		return alle, errors.New("domcfg == nil")
	}

	e, err := updateMemory(d, domcfg)
	if err != nil {
		log.Info("updateMemory() failed")
		return alle, err
	}
	if e != nil {
		alle = append(alle, e)
	}

	// update arch & machine
	if (domcfg.OS != nil) && (domcfg.OS.Type != nil) {
		// OS Type: &{Arch:x86_64 Machine:pc-i440fx-5.2 Type:hvm}
		t := domcfg.OS.Type
		if d.pb.QemuArch != t.Arch {
			e := NewChangeEvent(d.pb, "Droplet.QemuArch", d.pb.QemuArch, t.Arch)
			alle = append(alle, e)
			d.pb.QemuArch = t.Arch
		}
		if d.pb.QemuMachine != t.Machine {
			e := NewChangeEvent(d.pb, "Droplet.QemuMachine", d.pb.QemuMachine, t.Machine)
			alle = append(alle, e)
			d.pb.QemuMachine = t.Machine
		}
	}

	// check cpus
	if d.pb.Cpus != int64(domcfg.VCPU.Value) {
		// fmt.Printf("cpus changed. VCPU = %+v\n", domcfg.VCPU)
		fmt.Printf("cpus changed. from %d to %d\n", d.pb.Cpus, domcfg.VCPU.Value)
		alle = append(alle, NewChangeEvent(d.pb, "Droplet.Cpus", d.pb.Cpus, domcfg.VCPU.Value))
		d.pb.Cpus = int64(domcfg.VCPU.Value)
	}

	// update spice port
	if domcfg.Devices.Graphics != nil {
		for _, g := range domcfg.Devices.Graphics {
			if g.Spice == nil {
				continue
			}
			var s *libvirtxml.DomainGraphicSpice
			s = g.Spice
			// fmt.Printf("Spice: %d %+v %s\n", i, s, s.AutoPort)
			if s.AutoPort == "yes" {
				// should ignore either way
			} else {
				if d.pb.SpicePort != int64(s.Port) {
					// print out, but ignore the port number
					d.pb.SpicePort = int64(s.Port)
					fmt.Printf("Spice Port set to = %d\n", s.Port)
					alle = append(alle, NewChangeEvent(d.pb, "Droplet.SpicePort", d.pb.SpicePort, s.Port))
				}
			}
		}
	}

	// check type
	if domcfg.Type != "kvm" {
		fmt.Printf("not kvm. Virt type == %s\n", domcfg.Type)
		return alle, errors.New("not kvm")
	}

	nete, err := updateNetwork(d, domcfg)
	if err != nil {
		log.Info("updateNetwork() failed", err)
		return alle, err
	}

	for _, e := range nete {
		alle = append(alle, e)
	}

	nete, err = updateDisk(d, domcfg)
	if err != nil {
		return alle, err
	}

	for _, e := range nete {
		alle = append(alle, e)
	}

	if alle == nil {
		log.Info("libvirt xml import worked. nothing changed", domcfg.Name)
		return alle, nil
	}
	log.Info("libvirt xml import worked. droplet changed", domcfg.Name)

	// append each change event
	for _, e := range alle {
		me.events.Events = append(me.events.Events, e)
	}
	return alle, nil
}

// returns false if something went wrong
func updateMemory(d *DropletT, domcfg *libvirtxml.Domain) (*pb.Event, error) {
	if (d == nil) || (domcfg == nil) {
		return nil, errors.New("domcfg == nil")
	}

	if domcfg.Memory == nil {
		// nothing to do. libvirt xml file didn't define memory size
		return nil, nil
	}

	var m int64 = 0
	switch domcfg.Memory.Unit {
	case "KiB":
		m = int64(domcfg.Memory.Value * 1024)
	case "MiB":
		m = int64(domcfg.Memory.Value * 1024 * 1024)
	case "GiB":
		m = int64(domcfg.Memory.Value * 1024 * 1024 * 1024)
	default:
		fmt.Println("Unknown Memory Unit", domcfg.Memory.Unit)
		return nil, errors.New("Unknown Memory Unit " + domcfg.Memory.Unit)
	}
	e := d.SetMemory(m)
	if e != nil {
		fmt.Printf("Memory changed %s to %d %s\n", pb.HumanFormatBytes(d.pb.Memory), domcfg.Memory.Value, domcfg.Memory.Unit)
		d.pb.Memory = m
		// me.changed = true
	}
	return e, nil
}

func updateNetwork(d *DropletT, domcfg *libvirtxml.Domain) ([]*pb.Event, error) {
	var allEvents []*pb.Event
	if (d == nil) || (domcfg == nil) {
		return nil, errors.New("domcfg == nil")
	}

	// mac address & bridge name
	var macs map[string]string
	macs = make(map[string]string)
	// Iterate over the network interfaces and print the MAC addresses
	for _, iface := range domcfg.Devices.Interfaces {
		var hwaddr string
		var brname string
		// fmt.Printf("iface: %+v\n", iface)
		// fmt.Printf("MAC: %+v\n", iface.MAC)
		// fmt.Printf("Source: %+v\n", iface.Source)
		// fmt.Printf("Bridge: %+v\n", iface.Source.Bridge)
		// fmt.Printf("Model: %+v\n", iface.Model)
		if iface.MAC != nil {
			// iface.MAC.Address = "aa:bb:aa:bb:aa:ff"
			// log.Info("Interface:", iface.Target, "MAC Address:", iface.MAC.Address)
			// fmt.Printf("source: %+v\n", iface.Source)
			hwaddr = iface.MAC.Address
		}
		if iface.Source == nil {
			// fmt.Printf("non-standard network: %+v\n", iface)
			updatedXML, _ := xml.MarshalIndent(domcfg.Devices.Interfaces, "", "  ")
			log.Info("Non-Standard Network XML Start")
			fmt.Println(string(updatedXML))
			log.Info("Non-Standard Network XML End")
			return nil, errors.New("non-standard network. source == nil")
		}

		if iface.Source.Bridge == nil {
			if hwaddr == "" {
				fmt.Printf("non-standard network: %+v\n", iface)
				updatedXML, _ := xml.MarshalIndent(domcfg.Devices.Interfaces, "", "  ")
				log.Info("Non-Standard Network XML Start")
				fmt.Println(string(updatedXML))
				log.Info("Non-Standard Network XML End")
				return nil, errors.New("bridge is nil and no mac address")
			}
			brname = ""
		} else {
			if iface.Source.Bridge.Bridge == "" {
				if hwaddr == "" {
					fmt.Printf("non-standard network: %+v\n", iface)
					fmt.Printf("iface.Mac: %+v\n", iface)
					updatedXML, _ := xml.MarshalIndent(domcfg.Devices.Interfaces, "", "  ")
					log.Info("Non-Standard Network XML Start")
					fmt.Println(string(updatedXML))
					log.Info("Non-Standard Network XML End")
					return nil, errors.New("bridge is blank and no mac address")
				}
				brname = iface.Source.Bridge.Bridge
			}
		}
		// log.Info("network has bridge:", iface.Source.Bridge.Bridge)
		if hwaddr == "" {
			hwaddr = "generate " + domcfg.Name
			log.Info("need to generate mac addr for bridge:", brname)
			// return nil, errors.New("need to generate mac addr for bridge: " + brname)
		}
		macs[hwaddr] = brname
	}

	for mac, brname := range macs {
		var found bool = false
		// log.Info("XML has mac address:", mac, brname)
		for _, eth := range d.pb.Networks {
			if eth.Mac == mac {
				// log.Info("OKAY. FOUND ETH:", eth.Mac, eth.Name, brname)
				found = true
				if brname == "" {
					// if new bridge name is blank, keep the old one
					brname = eth.Name
				}
				if eth.Name != brname {
					if argv.IgnoreBr {
						log.Info("network was:", eth.Mac, eth.Name)
						log.Info("network now:", eth.Mac, brname)
						log.Info("ignoring network change (--xml-ignore-net)")
					} else {
						return nil, errors.New("bridge name changed")
					}
				}
			}
		}
		if !found {
			if checkUniqueMac(mac) {
			} else {
				log.Info("droplet", d.pb.Hostname, "duplicate mac address", mac)
				return nil, errors.New("duplicate mac address")
			}
			var eth *pb.Network
			eth = new(pb.Network)
			eth.Mac = mac
			if brname == "" {
				brname = "worldbr"
			}
			eth.Name = brname
			d.pb.Networks = append(d.pb.Networks, eth)
			allEvents = append(allEvents, NewChangeEvent(d.pb, "Droplet NewNetwork", "", mac+" "+brname))
		}
	}

	log.Verbose("mac addrs:", macs)
	return allEvents, nil
}

/* from vm3-with-nvme-1.5GB-sec.xml
   <disk type='block' device='disk'>
     <driver name='qemu' type='raw'/>
     <source dev='/dev/nvme4n1'/>
     <backingStore/>
     <target dev='vdb' bus='virtio'/>
     <alias name='virtio-disk1'/>
     <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
   </disk>
*/

// returns false if something went wrong
func updateDisk(d *DropletT, domcfg *libvirtxml.Domain) ([]*pb.Event, error) {
	var alle []*pb.Event

	if (d == nil) || (domcfg == nil) {
		return nil, errors.New("domcfg == nil")
	}
	for _, disk := range domcfg.Devices.Disks {
		var t *libvirtxml.DomainDiskSourceFile
		t = disk.Source.File
		if t == nil {
			fmt.Println("disk.Source.File == nil")
			continue
		}
		filename := t.File
		if filename == "" {
			fmt.Println("No disk source file found.")
			continue
		}

		e, err := insertFilename(d.pb, filename)
		if err != nil {
			return alle, err
		}
		if e == nil {
			continue
		}
		alle = append(alle, e)

		/*
			var found bool = false
			for _, disk := range d.pb.Disks {
				if disk.Filename == filename {
					log.Verbose("OKAY. FOUND filename", filename)
					found = true
				}
			}
			if !found {
				var disk *pb.Disk
				disk = new(pb.Disk)
				disk.Filename = filename
				d.pb.Disks = append(d.pb.Disks, disk)
				log.Info("New filename", filename)
				me.changed = true
			}
		*/
	}
	return alle, nil
}