package tart

import (
	"context"
	"encoding/json"
	"fmt"
	"maps"
	"strconv"
	"strings"
	"sync"

	"github.com/cirruslabs/cirrus-cli/internal/worker/chacha"
	"github.com/cirruslabs/echelon"
	"github.com/getsentry/sentry-go"
	"go.opentelemetry.io/otel/trace"
)

type VM struct {
	ident              string
	env                map[string]string
	runningVMCtx       context.Context
	runningVMCtxCancel context.CancelFunc
	wg                 sync.WaitGroup
	errChan            chan error
}

type Info struct {
	OS string `json:"os"`
}

type directoryMount struct {
	Name     string
	Path     string
	Tag      string
	ReadOnly bool
}

func NewVMClonedFrom(
	ctx context.Context,
	from string,
	to string,
	lazyPull bool,
	env map[string]string,
	chacha *chacha.Chacha,
	logger *echelon.Logger,
) (*VM, error) {
	runningVMCtx, runningVMCtxCancel := context.WithCancel(
		trace.ContextWithSpan(context.Background(), trace.SpanFromContext(ctx)),
	)

	vm := &VM{
		ident:              to,
		env:                env,
		runningVMCtx:       runningVMCtx,
		runningVMCtxCancel: runningVMCtxCancel,
		errChan:            make(chan error, 1),
	}

	pullLogger := logger.Scoped("pull virtual machine")
	if !lazyPull {
		vm.pull(ctx, from, chacha, pullLogger)
	} else {
		pullLogger.FinishWithType(echelon.FinishTypeSkipped)
	}

	cloneLogger := logger.Scoped("clone virtual machine")
	cloneLogger.Infof("Cloning virtual machine %s...", from)
	cloneEnv := vm.env
	if cloneEnv == nil {
		cloneEnv = map[string]string{}
	} else {
		cloneEnv = maps.Clone(cloneEnv)
	}

	cloneEnv["TART_NO_AUTO_PRUNE"] = ""

	if _, _, err := CmdWithLogger(ctx, cloneEnv, cloneLogger, "clone", from, vm.ident); err != nil {
		cloneLogger.Finish(false)
		return nil, err
	}

	cloneLogger.Finish(true)
	return vm, nil
}

func (vm *VM) pull(ctx context.Context, from string, chacha *chacha.Chacha, logger *echelon.Logger) {
	if chacha != nil && chacha.EnableTart() {
		logger.Infof("Pulling virtual machine %s via Chacha...", from)

		args := []string{
			"--proxy", chacha.Addr(),
			"--ca-cert", chacha.CertPath(),
			"--max-retries", "1",
			from,
		}

		if _, _, err := CmdWithLogger(ctx, vm.env, logger, "pull", args...); err != nil {
			logger.Errorf("Failed to pull virtual machine %s via Chacha: %v", from, err)
		} else {
			logger.FinishWithType(echelon.FinishTypeSucceeded)

			return
		}
	}

	logger.Infof("Pulling virtual machine %s...", from)

	if _, _, err := CmdWithLogger(ctx, vm.env, logger, "pull", from); err != nil {
		logger.Errorf("Ignoring pull failure: %v", err)
		logger.FinishWithType(echelon.FinishTypeFailed)
	} else {
		logger.FinishWithType(echelon.FinishTypeSucceeded)
	}
}

func (vm *VM) Ident() string {
	return vm.ident
}

func (vm *VM) Configure(
	ctx context.Context,
	cpu uint32,
	memory uint32,
	diskSize uint32,
	display string,
	logger *echelon.Logger,
) error {
	configureLogger := logger.Scoped("configure virtual machine")
	configureLogger.Infof("Configuring virtual machine %s...", vm.ident)

	_, _, err := CmdWithLogger(ctx, vm.env, configureLogger, "set", vm.ident, "--random-mac")
	if err != nil {
		configureLogger.Finish(false)
		return err
	}

	if cpu != 0 {
		cpuStr := strconv.FormatUint(uint64(cpu), 10)

		_, _, err := CmdWithLogger(ctx, vm.env, configureLogger, "set", vm.ident, "--cpu", cpuStr)
		if err != nil {
			configureLogger.Finish(false)
			return err
		}
	}

	if memory != 0 {
		memoryStr := strconv.FormatUint(uint64(memory), 10)

		_, _, err := CmdWithLogger(ctx, vm.env, configureLogger, "set", vm.ident, "--memory", memoryStr)
		if err != nil {
			configureLogger.Finish(false)
			return err
		}
	}

	if diskSize != 0 {
		diskSizeStr := strconv.FormatUint(uint64(diskSize), 10)

		_, _, err := CmdWithLogger(ctx, vm.env, configureLogger, "set", vm.ident, "--disk-size", diskSizeStr)
		if err != nil {
			configureLogger.Finish(false)
			return err
		}
	}

	if display != "" {
		_, _, err := CmdWithLogger(ctx, vm.env, configureLogger, "set", vm.ident, "--display", display)
		if err != nil {
			configureLogger.Finish(false)
			return err
		}
	}

	configureLogger.Finish(true)

	return nil
}

func (vm *VM) Start(
	ctx context.Context,
	softnet bool,
	softnetAllow []string,
	nested bool,
	directoryMounts []directoryMount,
) {
	vm.wg.Add(1)

	go func() {
		defer vm.wg.Done()

		args := []string{"--no-graphics", "--no-clipboard", "--no-audio", "--root-disk-opts", "sync=none"}

		if softnet {
			args = append(args, "--net-softnet")

			if len(softnetAllow) > 0 {
				args = append(args, "--net-softnet-allow", strings.Join(softnetAllow, ","))
			}
		}

		if nested {
			args = append(args, "--nested")
		}

		for _, directoryMount := range directoryMounts {
			var opts []string

			if tag := directoryMount.Tag; tag != "" {
				opts = append(opts, fmt.Sprintf("tag=%s", tag))
			}

			if directoryMount.ReadOnly {
				opts = append(opts, "ro")
			}

			dirArgumentValue := fmt.Sprintf("%s:%s", directoryMount.Name, directoryMount.Path)

			if len(opts) != 0 {
				dirArgumentValue += ":" + strings.Join(opts, ",")
			}

			args = append(args, "--dir", dirArgumentValue)
		}

		args = append(args, vm.ident)

		stdout, stderr, err := Cmd(vm.runningVMCtx, vm.env, "run", args...)
		if localHub := sentry.GetHubFromContext(ctx); localHub != nil {
			localHub.AddBreadcrumb(&sentry.Breadcrumb{
				Message: "\"tart run\" finished",
				Data: map[string]interface{}{
					"err":    err,
					"stdout": stdout,
					"stderr": stderr,
				},
			}, nil)
		}
		vm.errChan <- err
	}()
}

func (vm *VM) ErrChan() chan error {
	return vm.errChan
}

func (vm *VM) RetrieveIP(ctx context.Context) (string, error) {
	// wait 30 seconds since usually a VM boots in 15
	stdout, _, err := Cmd(ctx, vm.env, "ip", "--wait", "30", vm.ident)
	if err != nil {
		return "", err
	}

	return strings.TrimSpace(stdout), nil
}

func (vm *VM) Info(ctx context.Context) (*Info, error) {
	stdout, _, err := Cmd(ctx, vm.env, "get", "--format", "json", vm.ident)
	if err != nil {
		return nil, err
	}

	var info Info

	if err := json.Unmarshal([]byte(stdout), &info); err != nil {
		return nil, err
	}

	return &info, nil
}

func (vm *VM) Close() error {
	ctx := trace.ContextWithSpan(context.Background(), trace.SpanFromContext(vm.runningVMCtx))

	// Try to gracefully terminate the VM
	//nolint:dogsled // not interested in the output for now
	_, _, _ = Cmd(ctx, vm.env, "stop", "--timeout", "5", vm.ident)

	vm.runningVMCtxCancel()
	vm.wg.Wait()

	_, _, err := Cmd(ctx, vm.env, "delete", vm.ident)

	return err
}
