package server

import (
	"context"
	"encoding/json"
	"time"

	cmpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/configurationmanagement"
	spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound"
	tpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/transport"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/networkdomain"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/networkelement"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/plugin"
	"code.fbi.h-da.de/danet/gosdn/controller/store"
	"code.fbi.h-da.de/danet/gosdn/controller/topology"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/links"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/nodes"
	"code.fbi.h-da.de/danet/gosdn/controller/topology/ports"

	"github.com/google/uuid"
)

// ConfigurationManagementServer represents  ConfigurationManagementServer...
type ConfigurationManagementServer struct {
	cmpb.UnimplementedConfigurationManagementServiceServer
	pndStore        networkdomain.PndStore
	topologyService topology.Service
	nodeService     nodes.Service
	portService     ports.Service
	pluginService   plugin.Service
}

// NewConfigurationManagementServer creates the ConfigurationManagementServer..
func NewConfigurationManagementServer(
	pndStore networkdomain.PndStore,
	topologyService topology.Service,
	nodeService nodes.Service,
	portService ports.Service,
	pluginService plugin.Service,
) *ConfigurationManagementServer {
	return &ConfigurationManagementServer{
		pndStore:        pndStore,
		topologyService: topologyService,
		nodeService:     nodeService,
		portService:     portService,
		pluginService:   pluginService,
	}
}

// sdnConfig is used to parse the sdnConfig into JSON.
type sdnConfig struct {
	PndID           string                          `json:"pndID"`
	Nodes           []nodes.Node                    `json:"nodes"`
	Ports           []ports.Port                    `json:"ports"`
	Links           []links.Link                    `json:"links"`
	Plugins         []plugin.LoadedPlugin           `json:"plugins"`
	NetworkElements []networkelement.NetworkElement `json:"networkelements"`
}

// loadedSDNConfig is used to parse the stringified JSON sdnConfig into objects.
type loadedSDNConfig struct {
	PndID           string                                `json:"pndID"`
	Nodes           []nodes.Node                          `json:"nodes"`
	Ports           []ports.Port                          `json:"ports"`
	Links           []links.Link                          `json:"links"`
	Plugins         []plugin.LoadedPlugin                 `json:"plugins"`
	NetworkElements []networkelement.LoadedNetworkElement `json:"networkelements"`
}

// ExportSDNConfig returns the SDN configuration.
func (c ConfigurationManagementServer) ExportSDNConfig(ctx context.Context, request *cmpb.ExportSDNConfigRequest) (*cmpb.ExportSDNConfigResponse, error) {
	var sdnConfig = sdnConfig{}
	sdnConfig.PndID = request.Pid

	pndUUID := uuid.MustParse(request.Pid)
	pnd, err := c.pndStore.Get(store.Query{ID: pndUUID})
	if err != nil {
		return nil, err
	}

	networkElements := pnd.NetworkElements()

	sdnConfig.NetworkElements = networkElements

	sdnConfig.Nodes, err = c.nodeService.GetAll()
	if err != nil {
		return nil, err
	}
	sdnConfig.Ports, err = c.portService.GetAll()
	if err != nil {
		return nil, err
	}
	sdnConfig.Links, err = c.topologyService.GetAll()
	if err != nil {
		return nil, err
	}

	jsonSDNConfig, err := json.MarshalIndent(sdnConfig, "", "   ")
	if err != nil {
		return nil, err
	}

	sdnConfigDataString := string(jsonSDNConfig)

	return &cmpb.ExportSDNConfigResponse{
		Timestamp:     time.Now().UnixNano(),
		SdnConfigData: sdnConfigDataString,
		Status:        cmpb.Status_STATUS_OK}, nil
}

// ImportSDNConfig receives an SDN configuration and imports it.
func (c ConfigurationManagementServer) ImportSDNConfig(ctx context.Context, request *cmpb.ImportSDNConfigRequest) (*cmpb.ImportSDNConfigResponse, error) {
	pndUUID := uuid.MustParse(request.Pid)
	var sdnConfig = loadedSDNConfig{}
	err := json.Unmarshal([]byte(request.SdnConfigData), &sdnConfig)
	if err != nil {
		return nil, err
	}

	err = c.deleteAllElementsFromDatabase(pndUUID)
	if err != nil {
		return nil, err
	}

	err = c.createElementsFromSDNConfig(&sdnConfig, pndUUID)
	if err != nil {
		return nil, err
	}

	return &cmpb.ImportSDNConfigResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    cmpb.Status_STATUS_OK}, nil
}

func (c ConfigurationManagementServer) deleteAllElementsFromDatabase(pndUUID uuid.UUID) error {
	if err := c.deleteNetworkElements(pndUUID); err != nil {
		return err
	}

	if err := c.deletePlugins(); err != nil {
		return err
	}

	if err := c.deleteTopology(); err != nil {
		return err
	}

	return nil
}

func (c ConfigurationManagementServer) deleteTopology() error {
	links, err := c.topologyService.GetAll()
	if err != nil {
		return err
	}
	for _, link := range links {
		err = c.topologyService.DeleteLink(link)
		if err != nil {
			return err
		}
	}

	ports, err := c.portService.GetAll()
	if err != nil {
		return err
	}
	for _, port := range ports {
		err = c.portService.Delete(port)
		if err != nil {
			return err
		}
	}

	nodes, err := c.nodeService.GetAll()
	if err != nil {
		return err
	}
	for _, node := range nodes {
		err = c.nodeService.Delete(node)
		if err != nil {
			return err
		}
	}

	return nil
}

func (c ConfigurationManagementServer) deleteNetworkElements(pndUUID uuid.UUID) error {
	pnd, err := c.pndStore.Get(store.Query{ID: pndUUID})
	if err != nil {
		return err
	}

	networkElements := pnd.NetworkElements()
	for _, networkElement := range networkElements {
		err = pnd.RemoveNetworkElement(networkElement.ID())
		if err != nil {
			return err
		}
	}

	return nil
}

func (c ConfigurationManagementServer) deletePlugins() error {
	plugins, err := c.pluginService.GetAll()
	if err != nil {
		return err
	}
	for _, plugin := range plugins {
		err = c.pluginService.Delete(plugin)
		if err != nil {
			return err
		}
	}

	return nil
}

func (c ConfigurationManagementServer) createElementsFromSDNConfig(sdnConfig *loadedSDNConfig, pndUUID uuid.UUID) error {
	if err := c.createTopology(sdnConfig); err != nil {
		return err
	}

	if err := c.createNetworkElements(sdnConfig, pndUUID); err != nil {
		return err
	}

	return nil
}

func (c ConfigurationManagementServer) createTopology(sdnConfig *loadedSDNConfig) error {
	for _, inputNode := range sdnConfig.Nodes {
		node := nodes.Node{
			ID:   inputNode.ID,
			Name: inputNode.Name,
		}
		_, err := c.nodeService.EnsureExists(node)
		if err != nil {
			return err
		}
	}

	for _, inputPort := range sdnConfig.Ports {
		port := ports.Port{
			ID:            inputPort.ID,
			Name:          inputPort.Name,
			Configuration: inputPort.Configuration,
		}
		_, err := c.portService.EnsureExists(port)
		if err != nil {
			return err
		}
	}

	for _, inputPort := range sdnConfig.Links {
		sourceNode, err := c.nodeService.Get(store.Query{ID: inputPort.SourceNode.ID})
		if err != nil {
			return err
		}
		targetNode, err := c.nodeService.Get(store.Query{ID: inputPort.TargetNode.ID})
		if err != nil {
			return err
		}
		sourcePort, err := c.portService.Get(store.Query{ID: inputPort.SourcePort.ID})
		if err != nil {
			return err
		}
		targetPort, err := c.portService.Get(store.Query{ID: inputPort.TargetPort.ID})
		if err != nil {
			return err
		}
		link := links.Link{
			ID:         inputPort.ID,
			Name:       inputPort.Name,
			SourceNode: sourceNode,
			TargetNode: targetNode,
			SourcePort: sourcePort,
			TargetPort: targetPort,
		}
		err = c.topologyService.AddLink(link)
		if err != nil {
			return err
		}
	}

	return nil
}

func (c ConfigurationManagementServer) createNetworkElements(sdnConfig *loadedSDNConfig, pndUUID uuid.UUID) error {
	pnd, err := c.pndStore.Get(store.Query{ID: pndUUID})
	if err != nil {
		return err
	}

	for _, inputNetworkElement := range sdnConfig.NetworkElements {
		transportOption := tpb.TransportOption{
			Address:  inputNetworkElement.TransportAddress,
			Username: inputNetworkElement.TransportUsername,
			Password: inputNetworkElement.TransportPassword,
			TransportOption: &tpb.TransportOption_GnmiTransportOption{
				GnmiTransportOption: &tpb.GnmiTransportOption{},
			},
			// TODO: change TransportOption - type is not needed; this should
			// be removed as soon as we remove the csbi device type
			Type: spb.Type_TYPE_OPENCONFIG,
		}
		_, err := pnd.AddNetworkElement(
			inputNetworkElement.Name,
			&transportOption,
			nil,
			uuid.MustParse(inputNetworkElement.Plugin),
			uuid.MustParse(inputNetworkElement.ID),
		)
		if err != nil {
			return err
		}

		networkelement, err := pnd.GetNetworkElement(inputNetworkElement.ID)
		if err != nil {
			return err
		}

		err = pnd.UpdateNetworkElement(networkelement.ID(), inputNetworkElement.Model)
		if err != nil {
			return err
		}
	}
	return nil
}