package plugin

import (
	"encoding/json"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"regexp"

	"code.fbi.h-da.de/danet/gosdn/controller/customerrs"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/util"
	"code.fbi.h-da.de/danet/gosdn/controller/plugin/shared"
	"github.com/google/uuid"
	hcplugin "github.com/hashicorp/go-plugin"
	"go.mongodb.org/mongo-driver/bson"
	"gopkg.in/yaml.v3"
)

// State represents the current state of a plugin within the controller. Since
// the plugins used within the controller are basic go plugins, they can be
// CREATED, BUILT, LOADED or FAULTY. A plugin can not be unloaded (this is a
// limitation of go plugins in general).
type State int64

const (
	//CREATED state describes a plugin which has been created but is not yet
	//built.
	CREATED State = iota
	// BUILT state describes a plugin which is built and can be loaded into the
	// controller.
	BUILT
	// LOADED state describes a plugin which is running within the controller.
	LOADED
	// FAULTY state describes a plugin which couldn't be built or loaded.
	FAULTY
)

// Plugin describes an interface for a plugin within the controller. A plugin
// is based on hashicorp's `go plugin`.
type Plugin interface {
	ID() uuid.UUID
	GetClient() *hcplugin.Client
	State() State
	Manifest() *Manifest
	ExecPath() string
	Update() error
	Ping() error
	Restart() error
	Close()
	shared.DeviceModel
}

// Manifest represents the manifest of a plugin.
type Manifest struct {
	// Name of the plugin
	Name string `yaml:"name" json:"name" bson:"name"`
	// Name of the plugin
	Firmware string `yaml:"firmware" json:"firmware" bson:"firmware"`
	// Author of the plugin
	Author string `yaml:"author" json:"author" bson:"author"`
	// Version of the plugin
	Version string `yaml:"version" json:"version" bson:"version"`
}

// Validate is a method to check if the manifest is valid and is compliant with
// the requirements.
func (m *Manifest) Validate() error {
	errs := []error{}
	if m.Name == "" {
		errs = append(errs, fmt.Errorf("Name is required"))
	}
	if m.Firmware == "" {
		errs = append(errs, fmt.Errorf("Firmware is required"))
	}
	if m.Author == "" {
		errs = append(errs, fmt.Errorf("Author is required"))
	}
	if m.Version == "" {
		errs = append(errs, fmt.Errorf("Version is required"))
	}
	// regex from: https://stackoverflow.com/a/68921827
	validVersion, err := regexp.MatchString(`^([1-9]\d*|0)(\.(([1-9]\d*)|0)){2}$`,
		m.Version)
	if err != nil {
		errs = append(errs, err)
	}
	if !validVersion {
		errs = append(errs, fmt.Errorf("Version has to be of form: X.X.X"))
	}
	if len(errs) != 0 {
		return customerrs.CombinedErrListError{Errors: errs}
	}
	return nil
}

// ReadManifestFromFile reads a manifest file and returns a pointer to a newly
// created Manifest.
func ReadManifestFromFile(path string) (*Manifest, error) {
	manifest := &Manifest{}

	manifestFile, err := os.ReadFile(filepath.Join(path, util.ManifestFileName))
	if err != nil {
		return nil, err
	}

	err = yaml.Unmarshal(manifestFile, manifest)
	if err != nil {
		return nil, err
	}

	// validate the loaded manifest
	if err := manifest.Validate(); err != nil {
		return nil, err
	}

	return manifest, nil
}

type LoadedPlugin struct {
	// ID represents the UUID of the LoadedPlugin.
	ID string `json:"id" bson:"_id"`
	// Manifest represents the manifest of the LoadedPlugin.
	Manifest Manifest `json:"manifest" bson:"manifest"`
	// State represents the state of the LoadedPlugin.
	State State `json:"state,omitempty" bson:"state"`
	// ExecPath represents the path to the executable of the plugin.
	ExecPath string `json:"exec_path,omitempty" bson:"exec_path"`
	// ReattachConfig represents the configuration to reattach to a already
	// running plugin.
	ReattachConfig hcplugin.ReattachConfig `json:"reattatch_config,omitempty" bson:"reattatch_config"`
}

func (lp *LoadedPlugin) UnmarshalBSON(data []byte) error {
	loadedPluginHelper := new(LoadedPluginHelper)
	if err := bson.Unmarshal(data, loadedPluginHelper); err != nil {
		return err
	}

	lp.ID = loadedPluginHelper.ID
	lp.Manifest = loadedPluginHelper.Manifest
	lp.State = loadedPluginHelper.State
	lp.ExecPath = loadedPluginHelper.ExecPath
	lp.ReattachConfig = hcplugin.ReattachConfig{
		Protocol:        hcplugin.Protocol(loadedPluginHelper.ReattachConfig.Protocol),
		ProtocolVersion: loadedPluginHelper.ReattachConfig.ProtocolVersion,
		Addr: &net.UnixAddr{
			Name: loadedPluginHelper.ReattachConfig.Addr.Name,
			Net:  loadedPluginHelper.ReattachConfig.Addr.Net,
		},
		Pid:  loadedPluginHelper.ReattachConfig.Pid,
		Test: loadedPluginHelper.ReattachConfig.Test,
	}

	return nil
}

func (lp *LoadedPlugin) UnmarshalJSON(data []byte) error {
	loadedPluginHelper := new(LoadedPluginHelper)
	if err := json.Unmarshal(data, loadedPluginHelper); err != nil {
		return err
	}

	lp.ID = loadedPluginHelper.ID
	lp.Manifest = loadedPluginHelper.Manifest
	lp.State = loadedPluginHelper.State
	lp.ExecPath = loadedPluginHelper.ExecPath
	lp.ReattachConfig = hcplugin.ReattachConfig{
		Protocol:        hcplugin.Protocol(loadedPluginHelper.ReattachConfig.Protocol),
		ProtocolVersion: loadedPluginHelper.ReattachConfig.ProtocolVersion,
		Addr: &net.UnixAddr{
			Name: loadedPluginHelper.ReattachConfig.Addr.Name,
			Net:  loadedPluginHelper.ReattachConfig.Addr.Net,
		},
		Pid:  loadedPluginHelper.ReattachConfig.Pid,
		Test: loadedPluginHelper.ReattachConfig.Test,
	}

	return nil
}

type LoadedPluginHelper struct {
	ID             string               `json:"id" bson:"_id"`
	Manifest       Manifest             `json:"manifest" bson:"manifest"`
	State          State                `json:"state,omitempty" bson:"state"`
	ExecPath       string               `json:"exec_path,omitempty" bson:"exec_path"`
	ReattachConfig LoadedReattachConfig `json:"reattatch_config,omitempty" bson:"reattatch_config"`
}

type LoadedReattachConfig struct {
	Protocol        string
	ProtocolVersion int
	Addr            LoadedAddress
	Pid             int
	Test            bool
}

type LoadedAddress struct {
	Name string
	Net  string
}
