package nucleus

import (
	"encoding/json"
	"fmt"
	"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 Plugin struct {
	UUID     uuid.UUID
	state    plugin.State
	execPath string
	manifest *plugin.Manifest
	client   *hcplugin.Client
	shared.DeviceModel
}

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

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

	// connect through grpc
	gRPCClient, err := client.Client()
	if err != nil {
		return nil, err
	}

	// Request the plugin
	raw, err := gRPCClient.Dispense("deviceModel")
	if err != nil {
		return nil, err
	}

	model, ok := raw.(shared.DeviceModel)
	if !ok {
		return nil, customerrs.InvalidTypeAssertionError{
			Value: raw,
			Type:  (*shared.DeviceModel)(nil),
		}
	}

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

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},
	})

	// connect through grpc
	gRPCClient, err := client.Client()
	if err != nil {
		return nil, err
	}

	// Request the plugin
	raw, err := gRPCClient.Dispense("deviceModel")
	if err != nil {
		return nil, err
	}

	model, ok := raw.(shared.DeviceModel)
	if !ok {
		return nil, customerrs.InvalidTypeAssertionError{
			Value: model,
			Type:  (*shared.DeviceModel)(nil),
		}
	}

	return &Plugin{
		UUID:        uuid.MustParse(loadedPlugin.ID),
		client:      client,
		DeviceModel: model,
		manifest:    &loadedPlugin.Manifest,
		state:       plugin.CREATED,
	}, 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()
}

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

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
}

func (p *Plugin) Update() error {
	return fmt.Errorf("not implemented yet")
}

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

// Close ends the execution of the plugin.
func (p *Plugin) Close() {
	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(),
	})
}
