// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.

// Container resource
package resource

import (
	"context"
	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
	"github.com/docker/docker/api/types/mount"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/docker/api/types/strslice"
	"github.com/docker/docker/client"
	"github.com/docker/go-connections/nat"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"gopkg.in/yaml.v3"
	_ "gopkg.in/yaml.v3"
	"log/slog"
	"net/url"
_	"os"
_	"os/exec"
	"path/filepath"
_	"strings"
	"encoding/json"
	"io"
	"gitea.rosskeen.house/rosskeen.house/machine"
)

type ContainerClient interface {
	ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
	ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error
	ContainerList(context.Context, container.ListOptions) ([]types.Container, error)
	ContainerInspect(context.Context, string) (types.ContainerJSON, error)
	ContainerRemove(context.Context, string, container.RemoveOptions) error
	Close() error
}

type Container struct {
	Id              string                `json:"ID,omitempty" yaml:"ID,omitempty"`
	Name            string                `json:"name" yaml:"name"`
	Path            string                `json:"path" yaml:"path"`
	Cmd             []string              `json:"cmd,omitempty" yaml:"cmd,omitempty"`
	Entrypoint      strslice.StrSlice     `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"`
	Args            []string              `json:"args,omitempty" yaml:"args,omitempty"`
	Ports           []string              `json:"ports,omitempty" yaml:"ports,omitempty"`
	Environment     map[string]string     `json:"environment" yaml:"environment"`
	Image           string                `json:"image" yaml:"image"`
	ResolvConfPath  string                `json:"resolvconfpath" yaml:"resolvconfpath"`
	HostnamePath    string                `json:"hostnamepath" yaml:"hostnamepath"`
	HostsPath       string                `json:"hostpath" yaml:"hostspath"`
	LogPath         string                `json:"logpath" yaml:"logpath"`
	Created         string                `json:"created" yaml:"created"`
	ContainerState  types.ContainerState  `json:"containerstate" yaml:"containerstate"`
	RestartCount    int                   `json:"restartcount" yaml:"restartcount"`
	Driver          string                `json:"driver" yaml:"driver"`
	Platform        string                `json:"platform" yaml:"platform"`
	MountLabel      string                `json:"mountlabel" yaml:"mountlabel"`
	ProcessLabel    string                `json:"processlabel" yaml:"processlabel"`
	AppArmorProfile string                `json:"apparmorprofile" yaml:"apparmorprofile"`
	ExecIDs         []string              `json:"execids" yaml:"execids"`
	HostConfig      container.HostConfig  `json:"hostconfig" yaml:"hostconfig"`
	GraphDriver     types.GraphDriverData `json:"graphdriver" yaml:"graphdriver"`
	SizeRw          *int64                `json:",omitempty" yaml:",omitempty"`
	SizeRootFs      *int64                `json:",omitempty" yaml:",omitempty"`
	Networks				[]string							`json:"networks,omitempty" yaml:"networks,omitempty"`
	/*
	   Mounts          []MountPoint
	   Config          *container.Config
	   NetworkSettings *NetworkSettings
	*/

	State string `yaml:"state,omitempty" json:"state,omitempty"`

	apiClient ContainerClient
}

func init() {
	ResourceTypes.Register("container", func(u *url.URL) Resource {
		c := NewContainer(nil)
		c.Name = filepath.Join(u.Hostname(), u.Path)
		return c
	})
}

func NewContainer(containerClientApi ContainerClient) *Container {
	var apiClient ContainerClient = containerClientApi
	if apiClient == nil {
		var err error
		apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
		if err != nil {
			panic(err)
		}
	}
	return &Container{
		apiClient: apiClient,
	}
}

func (c *Container) Clone() Resource {
	return &Container {
		Id: c.Id,
		Name: c.Name,
		Path: c.Path,
		Cmd: c.Cmd,
		Entrypoint: c.Entrypoint,
		Args: c.Args,
		Environment: c.Environment,
		Image: c.Image,
		ResolvConfPath: c.ResolvConfPath,
		HostnamePath: c.HostnamePath,
		HostsPath: c.HostsPath,
		LogPath: c.LogPath,
		Created: c.Created,
		ContainerState: c.ContainerState,
		RestartCount: c.RestartCount,
		Driver: c.Driver,
		Platform: c.Platform,
		MountLabel: c.MountLabel,
		ProcessLabel: c.ProcessLabel,
		AppArmorProfile: c.AppArmorProfile,
		ExecIDs: c.ExecIDs,
		HostConfig: c.HostConfig,
		GraphDriver: c.GraphDriver,
		SizeRw: c.SizeRw,
		SizeRootFs: c.SizeRootFs,
		Networks: c.Networks,
		State: c.State,
		apiClient: c.apiClient,
	}
}

func (c *Container) StateMachine() machine.Stater {
	return ProcessMachine()
}

func (c *Container) URI() string {
	return fmt.Sprintf("container://%s", c.Id)
}

func (c *Container) SetURI(uri string) error {
	resourceUri, e := url.Parse(uri)
	if e == nil {
		if resourceUri.Scheme == c.Type() {
			c.Name, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI()))
		} else {
			e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, c.Type())
		}
	}
	return e
}

func (c *Container) JSON() ([]byte, error) {
	return json.Marshal(c)
}

func (c *Container) Validate() error {
	return fmt.Errorf("failed")
}

func (c *Container) Apply() error {
	ctx := context.Background()
	switch c.State {
	case "absent":
		return c.Delete(ctx)
	case "present":
		return c.Create(ctx)
	}
	return nil
}

func (c *Container) Load(r io.Reader) error {
	d := NewYAMLDecoder(r)
	return d.Decode(c)
}

func (c *Container) LoadDecl(yamlResourceDeclaration string) error {
	d := NewYAMLStringDecoder(yamlResourceDeclaration)
	return d.Decode(c)
}

func (c *Container) Create(ctx context.Context) error {
	numberOfEnvironmentVariables := len(c.Environment)

	portset := nat.PortSet {}
	for _, port := range c.Ports {
		portset[nat.Port(port)] = struct{}{}
	}
	config := &container.Config{
		Image:      c.Image,
		Cmd:        c.Cmd,
		Entrypoint: c.Entrypoint,
		Tty:        false,
		ExposedPorts: portset,
	}

	config.Env = make([]string, numberOfEnvironmentVariables)
	index := 0
	for k, v := range c.Environment {
		config.Env[index] = k + "=" + v
		index++
	}
	for i := range c.HostConfig.Mounts {
		if c.HostConfig.Mounts[i].Type == mount.TypeBind {
			if mountSourceAbsolutePath, e := filepath.Abs(c.HostConfig.Mounts[i].Source); e == nil {
				c.HostConfig.Mounts[i].Source = mountSourceAbsolutePath
			}
		}
	}

	networkConfig := &network.NetworkingConfig{
		EndpointsConfig: map[string]*network.EndpointSettings{},
	}

	settings := &network.EndpointSettings{}
	for _, network := range c.Networks {
		networkConfig.EndpointsConfig[network] = settings
	}

	resp, err := c.apiClient.ContainerCreate(ctx, config, &c.HostConfig, networkConfig, nil, c.Name)
	if err != nil {
		panic(err)
	}
	c.Id = resp.ID

	/*
	   statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
	   select {
	   case err := <-errCh:
	     if err != nil {
	       panic(err)
	     }
	   case <-statusCh:
	   }
	*/

	if startErr := c.apiClient.ContainerStart(ctx, c.Id, container.StartOptions{}); startErr != nil {
		return startErr
	}
	return err
}

// produce yaml representation of any resource

func (c *Container) Read(ctx context.Context) ([]byte, error) {
	var containerID string
	filterArgs := filters.NewArgs()
	filterArgs.Add("name", "/"+c.Name)
	containers, err := c.apiClient.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filterArgs,
	})

	if err != nil {
		panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name))
	}

	for _, container := range containers {
		for _, containerName := range container.Names {
			if containerName == "/"+c.Name {
				containerID = container.ID
			}
		}
	}

	containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID)
	if client.IsErrNotFound(err) {
		c.State = "absent"
	} else {
		c.State = "present"
		c.Id = containerJSON.ID
		if c.Name == "" {
			c.Name = containerJSON.Name
		}
		c.Path = containerJSON.Path
		c.Image = containerJSON.Image
		if containerJSON.State != nil {
			c.ContainerState = *containerJSON.State
		}
		c.Created = containerJSON.Created
		c.ResolvConfPath = containerJSON.ResolvConfPath
		c.HostnamePath = containerJSON.HostnamePath
		c.HostsPath = containerJSON.HostsPath
		c.LogPath = containerJSON.LogPath
		c.RestartCount = containerJSON.RestartCount
		c.Driver = containerJSON.Driver
	}
	slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
	return yaml.Marshal(c)
}

func (c *Container) Delete(ctx context.Context) error {
	err := c.apiClient.ContainerRemove(ctx, c.Id, container.RemoveOptions{
		RemoveVolumes: true,
		Force:         false,
	})
	if err != nil {
		slog.Error("Failed to remove: ", "Id", c.Id)
		panic(err)
	}
	return err
}

func (c *Container) Type() string { return "container" }

func (c *Container) ResolveId(ctx context.Context) string {
	filterArgs := filters.NewArgs()
	filterArgs.Add("name", "/"+c.Name)
	containers, err := c.apiClient.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filterArgs,
	})
	if err != nil {
		panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name))
	}

	for _, container := range containers {
		for _, containerName := range container.Names {
			if containerName == c.Name {
				if c.Id == "" {
					c.Id = container.ID
				}
				return container.ID
			}
		}
	}
	return ""
}
