package venvmanager

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"time"

	configMgmtPb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/configurationmanagement"
	mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
	ppb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/pnd"
	topologyPb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/topology"
	"code.fbi.h-da.de/danet/gosdn/applications/venv-manager/containerlab"
	link "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/links"
	"code.fbi.h-da.de/danet/gosdn/applications/venv-manager/node"
	port "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/port"
	topology "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/topology"
	yangparser "code.fbi.h-da.de/danet/gosdn/applications/venv-manager/yang-parser"
	"code.fbi.h-da.de/danet/gosdn/models/generated/openconfig"
	"github.com/openconfig/gnmi/proto/gnmi"
	"github.com/openconfig/ygot/ygot"

	"google.golang.org/grpc"
	"gopkg.in/yaml.v3"
)

// VenvManager is the object containing the core logic of the application.
type VenvManager struct {
	dialConnectionURL    string
	dialOption           grpc.DialOption
	topologyFilepath     string
	sdnConfigFilepath    string
	containerRegistryURL string
	pndID                string
	pndName              string
}

// NewVenvManager creates a new VenvManager to use.
func NewVenvManager(dialConnectionURL string, dialOption grpc.DialOption, topologyFilepath string, sdnConfigFilepath string, containerRegistryURL string) *VenvManager {
	v := new(VenvManager)
	v.dialConnectionURL = dialConnectionURL
	v.dialOption = dialOption
	v.topologyFilepath = topologyFilepath
	v.sdnConfigFilepath = sdnConfigFilepath
	v.containerRegistryURL = containerRegistryURL
	return v
}

func (v *VenvManager) createConnection() (*grpc.ClientConn, error) {
	conn, err := grpc.Dial(v.dialConnectionURL, v.dialOption, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(100*1024*1024)))
	if err != nil {
		return nil, err
	}

	return conn, nil
}

func (v *VenvManager) closeConnection(conn *grpc.ClientConn) {
	err := conn.Close()
	if err != nil {
		fmt.Println(err)
	}
}

// StartVirtualEnvironment uses containerlab to start the virtual environment.
func (v *VenvManager) StartVirtualEnvironment() error {
	cmd := exec.Command("sudo", "containerlab", "deploy", "-t", v.topologyFilepath)
	output, err := cmd.Output()
	if err != nil {
		return err
	}
	//Might be a good idea to switch to live output of the command later
	fmt.Println(string(output))

	return nil
}

// ReadAndSendSDNConfig gets the SDN config data and sends it to the controller.
func (v *VenvManager) ReadAndSendSDNConfig() error {
	sdnConfigData, err := v.readSDNConfigFile()
	if err != nil {
		return err
	}

	err = v.sendSDNConfigData(&sdnConfigData)
	if err != nil {
		return err
	}

	return nil
}

// sendSDNConfigData sends the sDN configuration data.
func (v *VenvManager) sendSDNConfigData(sdnConfigData *string) error {
	conn, err := v.createConnection()
	if err != nil {
		return err
	}
	defer v.closeConnection(conn)

	ctx := context.Background()

	pndService := ppb.NewPndServiceClient(conn)
	pndRes, err := pndService.GetPndList(ctx, &ppb.GetPndListRequest{Timestamp: getTimestamp()})
	if err != nil {
		return err
	}

	// currently only support for default PND
	v.pndID = pndRes.Pnd[0].Id

	configMgmtService := configMgmtPb.NewConfigurationManagementServiceClient(conn)

	_, err = configMgmtService.ImportSDNConfig(ctx, &configMgmtPb.ImportSDNConfigRequest{Timestamp: getTimestamp(), Pid: v.pndID, SdnConfigData: *sdnConfigData})
	if err != nil {
		return err
	}

	return nil
}

// CreateSDNConfigFile creates the SDN configuration file.
func (v *VenvManager) CreateSDNConfigFile() error {
	sdnConfigReponse, err := v.getSDNConfigData()
	if err != nil {
		return err
	}

	err = v.writeSDNConfigFile(*sdnConfigReponse)
	if err != nil {
		return err
	}

	return nil
}

// getSDNConfigData gets the sDN configuration data.
func (v *VenvManager) getSDNConfigData() (*string, error) {
	conn, err := v.createConnection()
	if err != nil {
		return nil, err
	}
	defer v.closeConnection(conn)

	ctx := context.Background()

	pndService := ppb.NewPndServiceClient(conn)
	pndRes, err := pndService.GetPndList(ctx, &ppb.GetPndListRequest{Timestamp: getTimestamp()})
	if err != nil {
		return nil, err
	}
	v.pndID = pndRes.Pnd[0].Id

	configMgmtService := configMgmtPb.NewConfigurationManagementServiceClient(conn)

	sdnConfigResponse, err := configMgmtService.ExportSDNConfig(ctx, &configMgmtPb.ExportSDNConfigRequest{Timestamp: getTimestamp(), Pid: v.pndID})
	if err != nil {
		return nil, err
	}

	return &sdnConfigResponse.SdnConfigData, nil
}

// writeSDNConfigFile writes the SDN configuration in a string to a file.
func (v *VenvManager) writeSDNConfigFile(sdnConfigToWrite string) error {
	err := os.WriteFile(v.sdnConfigFilepath, []byte(sdnConfigToWrite), 0644)
	if err != nil {
		return err
	}

	return nil
}

// readSDNConfigToFile reads the SDN configuration from a file to a string.
func (v *VenvManager) readSDNConfigFile() (string, error) {
	content, err := os.ReadFile(v.sdnConfigFilepath)
	if err != nil {
		return "", err
	}

	return string(content), nil
}

// CreateTopologyFile creates the topology file.
func (v *VenvManager) CreateTopologyFile() error {
	topologyData, err := v.getTopologyData()
	if err != nil {
		return err
	}

	parsedTopology, err := v.parseTopologyDataIntoStructs(topologyData)
	if err != nil {
		return err
	}

	enhancedTopology, err := v.getAndAddMoreData(parsedTopology)
	if err != nil {
		return err
	}

	containerlabTopology, err := v.parseIntoContainerlabTopology(enhancedTopology)
	if err != nil {
		return err
	}

	err = v.writeTopologyToYamlFile(containerlabTopology)
	if err != nil {
		return err
	}

	return nil
}

func (v *VenvManager) getTopologyData() (*topologyPb.GetTopologyResponse, error) {
	conn, err := v.createConnection()
	if err != nil {
		return nil, err
	}
	defer v.closeConnection(conn)

	ctx := context.Background()

	pndService := ppb.NewPndServiceClient(conn)
	pndRes, err := pndService.GetPndList(ctx, &ppb.GetPndListRequest{Timestamp: getTimestamp()})
	if err != nil {
		return nil, err
	}
	v.pndID = pndRes.Pnd[0].Id
	v.pndName = pndRes.Pnd[0].Name

	toplogyService := topologyPb.NewTopologyServiceClient(conn)
	topologyResponse, err := toplogyService.GetTopology(ctx, &topologyPb.GetTopologyRequest{Timestamp: getTimestamp()})
	if err != nil {
		return nil, err
	}

	return topologyResponse, nil
}

func (v *VenvManager) parseTopologyDataIntoStructs(topologyData *topologyPb.GetTopologyResponse) (*topology.GoSdnTopology, error) {
	topology := topology.GoSdnTopology{}

	links := []link.Link{}
	nodes := []node.Node{}
	ports := []port.Port{}

	for _, unparsedLink := range topologyData.Toplogy.Links {
		sourceNode := node.Node{ID: unparsedLink.SourceNode.Id, Name: unparsedLink.SourceNode.Name, Kind: "", Image: ""}
		targetNode := node.Node{ID: unparsedLink.TargetNode.Id, Name: unparsedLink.TargetNode.Name, Kind: "", Image: ""}
		sourcePort := port.Port{ID: unparsedLink.SourcePort.Id, Name: unparsedLink.SourcePort.Name}
		targetPort := port.Port{ID: unparsedLink.TargetPort.Id, Name: unparsedLink.TargetPort.Name}

		index, err := getIndexOfElement(nodes, sourceNode.ID)
		if err != nil {
			nodes = append(nodes, sourceNode)
		} else {
			sourceNode = nodes[index]
		}

		index, err = getIndexOfElement(nodes, targetNode.ID)
		if err != nil {
			nodes = append(nodes, targetNode)
		} else {
			targetNode = nodes[index]
		}

		index, err = getIndexOfElement(ports, sourcePort.ID)
		if err != nil {
			ports = append(ports, sourcePort)
		} else {
			sourcePort = ports[index]
		}

		index, err = getIndexOfElement(ports, targetPort.ID)
		if err != nil {
			ports = append(ports, targetPort)
		} else {
			targetPort = ports[index]
		}

		newLink := link.Link{ID: unparsedLink.Id, Name: unparsedLink.Name, SourceNode: &sourceNode, TargetNode: &targetNode, SourcePort: &sourcePort, TargetPort: &targetPort}
		links = append(links, newLink)
	}

	topology.Nodes = nodes
	topology.Ports = ports
	topology.Links = links

	return &topology, nil
}

func (v *VenvManager) loadNetworkElementModelPathsIntoGosdn(ctx context.Context, conn *grpc.ClientConn, nodes *[]node.Node) error {
	networkElementService := mnepb.NewNetworkElementServiceClient(conn)

	paths := [2]string{"/lldp/config/system-description", "/system/state/"}
	for _, path := range paths {
		for _, node := range *nodes {
			_, err := networkElementService.GetPath(ctx, &mnepb.GetPathRequest{Mneid: node.ID, Pid: v.pndID, Path: path})
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func (v *VenvManager) getAndAddMoreData(topologyData *topology.GoSdnTopology) (*topology.GoSdnTopology, error) {
	conn, err := v.createConnection()
	if err != nil {
		return nil, err
	}
	defer v.closeConnection(conn)

	ctx := context.Background()

	var path = "/"
	var ygotPath *gnmi.Path

	// Create 'root' path to be able to load the whole model from the store.
	ygotPath, err = ygot.StringToPath(path, ygot.StructuredPath)
	if err != nil {
		return nil, err
	}

	// just to load model data into goSDN to guaranteed have new data available for get request
	err = v.loadNetworkElementModelPathsIntoGosdn(ctx, conn, &topologyData.Nodes)
	if err != nil {
		return nil, err
	}

	networkElementService := mnepb.NewNetworkElementServiceClient(conn)

	for iterator, node := range topologyData.Nodes {
		getNetworkElementResponse, _ := networkElementService.Get(ctx, &mnepb.GetNetworkElementRequest{NetworkElementId: node.ID})
		if err != nil {
			return nil, err
		}

		var marshalledYangData openconfig.Device

		err = yangparser.Unmarshal([]byte(getNetworkElementResponse.NetworkElement.Model), ygotPath, &marshalledYangData)
		if err != nil {
			return nil, err
		}

		mgmntAddress := strings.Split(getNetworkElementResponse.NetworkElement.TransportAddress, ":")
		topologyData.Nodes[iterator].MgmtIpv4 = mgmntAddress[0]
		topologyData.Nodes[iterator].YangData = marshalledYangData
		topologyData.Nodes[iterator].FillAllFields(v.containerRegistryURL)
	}

	return topologyData, nil
}

func (v *VenvManager) parseIntoContainerlabTopology(topologyData *topology.GoSdnTopology) (*containerlab.YamlStruct, error) {
	containerlabTopology := containerlab.YamlStruct{}

	containerlabTopology.Name = v.pndName

	// find a better way than to do this
	var managementNet string
	if len(topologyData.Nodes) > 0 {
		managementNet = topologyData.Nodes[0].MgmtIpv4 + "/16"
	}

	containerlabTopology.Mgmt = containerlab.Management{Network: containerlabTopology.Name + "-network", Ipv4Subnet: managementNet}

	containerlabTopology.Topology = containerlab.Topology{Nodes: make(map[string]containerlab.Node), Links: make([]containerlab.Link, 0)}
	for _, node := range topologyData.Nodes {
		containerlabTopology.Topology.Nodes[node.Name] = containerlab.Node{Kind: node.Kind, Image: node.Image, MgmtIpv4: node.MgmtIpv4}
	}
	for _, link := range topologyData.Links {
		newLink := containerlab.Link{
			Endpoints: link.GetLinkAsSliceOfStrings(),
		}
		containerlabTopology.Topology.Links = append(containerlabTopology.Topology.Links, newLink)
	}

	return &containerlabTopology, nil
}

func (v *VenvManager) writeTopologyToYamlFile(containerlabStruct *containerlab.YamlStruct) error {
	yaml, err := yaml.Marshal(containerlabStruct)
	if err != nil {
		return err
	}

	err = os.WriteFile(v.topologyFilepath, yaml, 0644)
	if err != nil {
		return err
	}

	return nil
}

func getIndexOfElement[T hasGetID](items []T, id string) (int, error) {
	for index, arrayItem := range items {
		if arrayItem.GetID() == id {
			return index, nil
		}
	}

	return -1, errors.New("not found")
}

type hasGetID interface {
	GetID() string
}

func getTimestamp() int64 {
	return int64(time.Now().Nanosecond())
}
