package nucleus

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"

	"code.fbi.h-da.de/danet/gosdn/controller/customerrs"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/plugin"
	"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"
)

type pluginConnection struct {
	client *hcplugin.Client
	model  shared.DeviceModel
}

var pluginClients = make(map[uuid.UUID]pluginConnection, 0)

// Plugin is the controllers internal representation of a plugin.
type Plugin struct {
	UUID     uuid.UUID
	state    plugin.State
	execPath string
	manifest *plugin.Manifest
	client   *hcplugin.Client
	shared.DeviceModel
}

// NewPlugin creates a new Plugin.
func NewPlugin(id uuid.UUID, execPath string) (*Plugin, error) {
	client := hcplugin.NewClient(&hcplugin.ClientConfig{
		HandshakeConfig:  shared.Handshake,
		Plugins:          shared.PluginMap,
		Cmd:              exec.Command(filepath.Join(execPath, util.PluginExecutableName)),
		AllowedProtocols: []hcplugin.Protocol{hcplugin.ProtocolGRPC},
	})

	manifest, err := plugin.ReadManifestFromFile(execPath)
	if err != nil {
		return nil, err
	}

	// create a client that is within the AllowedProtocols. In this case this
	// returns a gRPCClient. Allows to connect through gRPC.
	gRPCClient, err := client.Client()
	if err != nil {
		return nil, err
	}

	// Request the plugin. This returns the gRPC client from the
	// DeviceModelPlugin. This can then be casted to the interface that we are
	// exposing through the plugin (in this case "DeviceModel").
	raw, err := gRPCClient.Dispense("deviceModel")
	if err != nil {
		return nil, err
	}

	// cast the raw plugin to the DeviceModel interface. This allows to call
	// methods on the plugin as if it were a normal DeviceModel instance but
	// actually they are executed on the plugin sent through gRPC.
	model, ok := raw.(shared.DeviceModel)
	if !ok {
		return nil, customerrs.InvalidTypeAssertionError{
			Value: raw,
			Type:  (*shared.DeviceModel)(nil),
		}
	}

	pluginClients[id] = pluginConnection{
		client: client,
		model:  model,
	}

	return &Plugin{
		UUID:        id,
		client:      client,
		execPath:    execPath,
		DeviceModel: model,
		manifest:    manifest,
		state:       plugin.CREATED,
	}, nil
}

// NewPluginThroughReattachConfig creates a new Plugin through a reattach config.
func NewPluginThroughReattachConfig(loadedPlugin plugin.LoadedPlugin) (plugin.Plugin, error) {
	//client := hcplugin.NewClient(&hcplugin.ClientConfig{
	//	HandshakeConfig:  shared.Handshake,
	//	Plugins:          shared.PluginMap,
	//	Reattach:         &loadedPlugin.ReattachConfig,
	//	AllowedProtocols: []hcplugin.Protocol{hcplugin.ProtocolGRPC},
	//})

	//// create a client that is within the AllowedProtocols. In this case this
	//// returns a gRPCClient. Allows to connect through gRPC.
	//gRPCClient, err := client.Client()
	//if err != nil {
	//	return nil, err
	//}

	//// Request the plugin. This returns the gRPC client from the
	//// DeviceModelPlugin. This can then be casted to the interface that we are
	//// exposing through the plugin (in this case "DeviceModel").
	//raw, err := gRPCClient.Dispense("deviceModel")
	//if err != nil {
	//	return nil, err
	//}

	//// cast the raw plugin to the DeviceModel interface. This allows to call
	//// methods on the plugin as if it were a normal DeviceModel instance but
	//// actually they are executed on the plugin sent through gRPC.
	//model, ok := raw.(shared.DeviceModel)
	//if !ok {
	//	return nil, customerrs.InvalidTypeAssertionError{
	//		Value: model,
	//		Type:  (*shared.DeviceModel)(nil),
	//	}
	//}
	pluginId, err := uuid.Parse(loadedPlugin.ID)
	if err != nil {
		return nil, err
	}

	pc, ok := pluginClients[pluginId]
	if !ok {
		return nil, fmt.Errorf("plugin not found")
	}

	return &Plugin{
		UUID:        uuid.MustParse(loadedPlugin.ID),
		client:      pc.client,
		DeviceModel: pc.model,
		manifest:    &loadedPlugin.Manifest,
		state:       plugin.INITIALIZED,
	}, nil
}

// ID returns the ID of the plugin.
func (p *Plugin) ID() uuid.UUID {
	return p.UUID
}

// ID returns the ID of the plugin.
func (p *Plugin) ReattachConfig() *hcplugin.ReattachConfig {
	return p.client.ReattachConfig()
}

// Remove ensures that the Plugin is killed and the corresponding files are
// removed.
func (p *Plugin) Remove() error {
	// stop the running plugins process
	p.Close()
	// remove the plugins folder
	return os.RemoveAll(p.ExecPath())
}

// State returns the current state of the plugin.
// Different states of the plugin can be:
//   - created
//   - initialized
//   - faulty
func (p *Plugin) State() plugin.State {
	return p.state
}

// ExecPath returns the path to the executable of the plugin.
func (p *Plugin) ExecPath() string {
	return p.execPath
}

// GetClient returns the client of the plugin.
func (p *Plugin) GetClient() *hcplugin.Client {
	return p.client
}

// Manifest returns the manifest of the plugin.
func (p *Plugin) Manifest() *plugin.Manifest {
	return p.manifest
}

// Update updates the plugin to the latest available version.
func (p *Plugin) Update() error {
	return fmt.Errorf("not implemented yet")
}

// Restart restarts the plugin.
func (p *Plugin) Restart() error {
	return fmt.Errorf("not implemented yet")
}

// Close ends the execution of the plugin.
func (p *Plugin) Close() {
	// end the plugin process
	p.client.Kill()
}

// Ping checks if the client connection is healthy.
func (p *Plugin) Ping() error {
	protocolClient, err := p.client.Client()
	if err != nil {
		return err
	}
	return protocolClient.Ping()
}

// TODO: update for the new way of handling plugins
// UpdatePlugin updates a given Plugin. Therefore the version of the
// `plugin.yml` manifest file is compared to the version in use. If a new
// version is within the plugin folder, the new version of the plugin is built.
func UpdatePlugin(p plugin.Plugin) (updated bool, err error) {
	return false, fmt.Errorf("not implemented yet")
}

func (p *Plugin) MarshalJSON() ([]byte, error) {
	return json.Marshal(&struct {
		ID             uuid.UUID                `json:"id,omitempty"`
		Manifest       *plugin.Manifest         `json:"manifest" bson:"manifest"`
		State          plugin.State             `json:"state,omitempty" bson:"state"`
		ExecPath       string                   `json:"exec_path,omitempty" bson:"exec_path"`
		ReattachConfig *hcplugin.ReattachConfig `json:"reattatch_config,omitempty" bson:"reattatch_config"`
	}{
		ID:             p.ID(),
		Manifest:       p.Manifest(),
		State:          p.State(),
		ExecPath:       p.ExecPath(),
		ReattachConfig: p.ReattachConfig(),
	})
}

func (p *Plugin) MarshalBSON() ([]byte, error) {
	return bson.Marshal(&struct {
		ID             string                   `bson:"_id,omitempty"`
		Manifest       *plugin.Manifest         `json:"manifest" bson:"manifest"`
		State          plugin.State             `json:"state,omitempty" bson:"state"`
		ExecPath       string                   `json:"exec_path,omitempty" bson:"exec_path"`
		ReattachConfig *hcplugin.ReattachConfig `json:"reattatch_config,omitempty" bson:"reattatch_config"`
	}{
		ID:             p.ID().String(),
		Manifest:       p.Manifest(),
		State:          p.State(),
		ExecPath:       p.ExecPath(),
		ReattachConfig: p.ReattachConfig(),
	})
}