package main

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"

	incus "github.com/lxc/incus/v6/client"
	"github.com/lxc/incus/v6/shared/api"
)

var (
	timeout     = 120
	killTimeout = 5
)

type BytesBuffer struct {
	*bytes.Buffer
}

func (*BytesBuffer) Close() error {
	return nil
}

type IncusClient struct {
	Client incus.InstanceServer
}

// Connect to the LXD socket.
func (c *IncusClient) Connect() error {
	client, err := incus.ConnectIncusUnix("", nil)
	if err != nil {
		return err
	}

	c.Client = client
	return nil
}

// Create a container using a specific remote and alias.
func (c *IncusClient) Create(name, remote, alias string) error {
	fmt.Printf("creating %s with %s:%s\n", name, remote, alias)

	if c.Client == nil {
		err := c.Connect()
		if err != nil {
			return err
		}
	}

	server := ""
	switch remote {
	case "images":
		server = "https://images.linuxcontainers.org"
	case "ubuntu":
		server = "https://cloud-images.ubuntu.com/releases"
	case "ubuntu-daily":
		server = "https://cloud-images.ubuntu.com/daily"
	default:
		return fmt.Errorf("unknown remote: %s", remote)
	}

	req := api.InstancesPost{
		Name: name,
		Source: api.InstanceSource{
			Type:     "image",
			Mode:     "pull",
			Protocol: "simplestreams",
			Server:   server,
			Alias:    alias,
		},
	}

	// Get LXD to create the container (background operation)
	op, err := c.Client.CreateInstance(req)
	if err != nil {
		return err
	}

	// Wait for the operation to complete
	err = op.Wait()
	if err != nil {
		return err
	}

	return nil
}

// Delete the given container.
func (c *IncusClient) Delete(name string) error {
	fmt.Println("deleting", name)
	op, err := c.Client.DeleteInstance(name)
	if err != nil {
		return err
	}

	return op.Wait()
}

// Run a command returning result struct. Will kill commands that take longer
// than 120 seconds.
func (c *IncusClient) Exec(name string, command ...string) error {
	fmt.Printf("$ %s\n", strings.Join(command, " "))

	cmd := []string{"/usr/bin/timeout", "-k", strconv.Itoa(killTimeout), strconv.Itoa(timeout)}
	cmd = append(cmd, command...)
	req := api.InstanceExecPost{
		Command:   cmd,
		WaitForWS: true,
	}

	output := &BytesBuffer{bytes.NewBuffer(nil)}

	args := incus.InstanceExecArgs{
		Stdout:   output,
		Stderr:   output,
		DataDone: make(chan bool),
	}

	op, err := c.Client.ExecInstance(name, req, &args)
	if err != nil {
		return err
	}

	err = op.Wait()
	if err != nil {
		return err
	}

	// Wait for any remaining I/O to be flushed
	<-args.DataDone

	// get the return code
	opAPI := op.Get()
	rc := int(opAPI.Metadata["return"].(float64))

	if rc != 0 {
		return errors.New(output.String())
	}

	fmt.Println(output.String())

	return nil
}

// Push file to container.
func (c *IncusClient) Push(name, src, dst string) error {
	fmt.Printf("cp %s %s%s\n", src, name, dst)
	f, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("error reading %s: %w", src, err)
	}
	defer f.Close()

	return c.Client.CreateInstanceFile(name, dst, incus.InstanceFileArgs{
		Content: f,
		Mode:    0644,
	})
}

// Start the given container.
func (c *IncusClient) Start(name string) error {
	fmt.Println("starting", name)
	reqState := api.InstanceStatePut{
		Action:  "start",
		Timeout: -1,
	}

	op, err := c.Client.UpdateInstanceState(name, reqState, "")
	if err != nil {
		return err
	}

	return op.Wait()
}

// Stop the given container.
func (c *IncusClient) Stop(name string) error {
	fmt.Println("stopping", name)
	reqState := api.InstanceStatePut{
		Action:  "stop",
		Force:   true,
		Timeout: 10,
	}

	op, err := c.Client.UpdateInstanceState(name, reqState, "")
	if err != nil {
		return err
	}

	return op.Wait()
}
