package daemon

import (
	"errors"
	"runtime"
	"sync"
	"syscall"

	"github.com/rs/zerolog/log"

	"code.dumpstack.io/tools/out-of-tree/api"
)

type Resources struct {
	initialized bool

	CPU *CPUResource
	RAM *RAMResources
}

func NewResources() (r *Resources) {
	r = &Resources{}
	r.CPU = NewCPUResources()
	r.RAM = NewRAMResources()
	r.initialized = true
	return
}

func (r *Resources) Allocate(job api.Job) (err error) {
	if !r.initialized {
		err = errors.New("resources not initialized")
		return
	}

	if job.Artifact.Qemu.Cpus == 0 {
		err = errors.New("no cpus requested")
		return
	}

	if job.Artifact.Qemu.Memory == 0 {
		err = errors.New("no memory requested")
		return
	}

	origRam := r.RAM.GetSpent()
	origCPU := r.CPU.GetSpent()

	err = r.CPU.Allocate(job.Artifact.Qemu.Cpus)
	if err != nil {
		return
	}

	err = r.RAM.Allocate(job.Artifact.Qemu.Memory)
	if err != nil {
		r.CPU.Release(job.Artifact.Qemu.Cpus)
		return
	}

	log.Debug().Msgf("allocated %d cpus, %d MB ram",
		r.CPU.GetSpent()-origCPU,
		r.RAM.GetSpent()-origRam)

	return
}

func (r *Resources) Release(job api.Job) {
	if !r.initialized {
		log.Error().Msg("resources not initialized")
		return
	}

	r.CPU.Release(job.Artifact.Qemu.Cpus)
	r.RAM.Release(job.Artifact.Qemu.Memory)

	log.Debug().Msgf("released %d cpus, %d MB ram",
		job.Artifact.Qemu.Cpus,
		job.Artifact.Qemu.Memory)
}

type CPUResource struct {
	num        int
	overcommit float64

	mu    *sync.Mutex
	spent int
}

const (
	Allocation = iota
	Release
)

func NewCPUResources() (cpur *CPUResource) {
	cpur = &CPUResource{}
	cpur.mu = &sync.Mutex{}
	cpur.num = runtime.NumCPU()
	cpur.overcommit = 1
	log.Debug().Msgf("total cpus: %d", cpur.num)
	return
}

func (cpur *CPUResource) SetOvercommit(oc float64) {
	log.Info().Int("cpus", cpur.num).
		Int("result", int(float64(cpur.num)*oc)).
		Msgf("%.02f", oc)
	cpur.overcommit = oc
}

func (cpur *CPUResource) GetSpent() int {
	cpur.mu.Lock()
	defer cpur.mu.Unlock()
	return cpur.spent
}

var ErrNotEnoughCpu = errors.New("not enough cpu")

func (cpur *CPUResource) Allocate(cpu int) (err error) {
	cpur.mu.Lock()
	defer cpur.mu.Unlock()

	if cpur.spent+cpu > int(float64(cpur.num)*cpur.overcommit) {
		err = ErrNotEnoughCpu
		return
	}

	cpur.spent += cpu
	return
}

func (cpur *CPUResource) Release(cpu int) (err error) {
	cpur.mu.Lock()
	defer cpur.mu.Unlock()

	if cpur.spent < cpu {
		err = ErrFreeingMoreThanAllocated
		return
	}

	cpur.spent -= cpu
	return
}

type RAMResources struct {
	mb         int
	overcommit float64

	mu    *sync.Mutex
	spent int
}

func NewRAMResources() (ramr *RAMResources) {
	ramr = &RAMResources{}
	ramr.mu = &sync.Mutex{}
	ramr.overcommit = 1

	var info syscall.Sysinfo_t
	syscall.Sysinfo(&info)
	ramr.mb = int(info.Totalram / 1024 / 1024)
	log.Debug().Msgf("total ram: %d MB", ramr.mb)
	return
}

func (ramr *RAMResources) SetOvercommit(oc float64) {
	log.Info().Int("ram", ramr.mb).
		Int("result", int(float64(ramr.mb)*oc)).
		Msgf("%.02f", oc)
	ramr.overcommit = oc
}

func (ramr RAMResources) GetSpent() int {
	ramr.mu.Lock()
	defer ramr.mu.Unlock()
	return ramr.spent
}

var ErrNotEnoughRam = errors.New("not enough ram")

func (ramr *RAMResources) Allocate(mb int) (err error) {
	ramr.mu.Lock()
	defer ramr.mu.Unlock()

	ocmem := int(float64(ramr.mb) * ramr.overcommit)

	if mb > ocmem-ramr.spent {
		err = ErrNotEnoughRam
		return
	}

	ramr.spent += mb
	return
}

var ErrFreeingMoreThanAllocated = errors.New("freeing more than allocated")

func (ramr *RAMResources) Release(mb int) (err error) {
	ramr.mu.Lock()
	defer ramr.mu.Unlock()

	if ramr.spent < mb {
		err = ErrFreeingMoreThanAllocated
		return
	}

	ramr.spent -= mb
	return
}