package csbi

import (
	"bytes"
	"context"
	"fmt"
	"io/fs"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound"
	"github.com/openconfig/ygot/genutil"
	"github.com/openconfig/ygot/gogen"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/viper"
	codes "google.golang.org/grpc/codes"
	"google.golang.org/grpc/peer"
	status "google.golang.org/grpc/status"
	"gopkg.in/yaml.v3"
)

// write takes a ygen.Generatedcode struct and writes the Go code
// snippets contained within it to the io.Writer, w, provided as an argument.
// The output includes a package header which is generated.
func write(ctx context.Context, code *gogen.GeneratedCode, path string, sbiType spb.Type) error {
	if err := os.Mkdir(path, 0755); err != nil {
		if err.(*fs.PathError).Err.Error() != "file exists" { //nolint:errorlint
			return err
		}
	}
	switch sbiType {
	case spb.Type_TYPE_PLUGIN:
		return writePlugin(code, path)
	case spb.Type_TYPE_CONTAINERISED:
		return writeCsbi(ctx, code, path)
	default:
		return fmt.Errorf("invalid sbi type provided")
	}
}

func removePort(ip net.Addr) (string, error) {
	addr, ok := ip.(*net.TCPAddr)
	if !ok {
		return "", fmt.Errorf("invalid type assertion")
	}
	return addr.IP.String(), nil
}

func writeCsbi(ctx context.Context, code *gogen.GeneratedCode, path string) error {
	p, ok := peer.FromContext(ctx)
	if !ok || p == nil {
		e := fmt.Errorf("no peer information in context %v", ctx)
		log.Error(e)
		return status.Errorf(codes.Aborted, "%v", e)
	}
	controller, err := removePort(p.Addr)
	if err != nil {
		log.Error(err)
		return status.Errorf(codes.Aborted, "%v", err)
	}
	target := ctx.Value("target-address")

	writerViper := viper.New()
	writerViper.Set("uuid", path)
	writerViper.Set("controller", net.JoinHostPort(controller, "55055"))
	writerViper.Set("target", target)

	if err := writerViper.WriteConfigAs(filepath.Join(path, ".csbi.toml")); err != nil {
		return err
	}

	if err := copyFile(path, "csbi.go"); err != nil {
		return err
	}

	if err := copyFile(path, gostructAdditionsName); err != nil {
		return err
	}

	if err := copyFile(path, "go.mod"); err != nil {
		return err
	}

	if err := copyFile(path, "go.sum"); err != nil {
		return err
	}

	if err := copyFile(path, "Dockerfile"); err != nil {
		return err
	}

	return writeGoStruct(path, code, spb.Type_TYPE_CONTAINERISED)
}

func writePlugin(code *gogen.GeneratedCode, path string) error {
	if err := copyFile(path, gostructAdditionsName); err != nil {
		return err
	}

	return writeGoStruct(path, code, spb.Type_TYPE_PLUGIN)
}

func copyFile(path, filename string) error {
	var stderr bytes.Buffer
	srcFile := filepath.Join("resources", filename)
	dstFile := filepath.Join(path, filename)
	cmd := exec.Command("cp", srcFile, dstFile)
	cmd.Stderr = &stderr
	err := cmd.Run()
	if err != nil {
		log.Error(stderr.String())
		return err
	}

	log.WithFields(log.Fields{
		"source file": srcFile,
		"dst file":    dstFile,
	}).Debugf("file copied")
	return nil
}

func writeGoStruct(path string, code *gogen.GeneratedCode, t spb.Type) error {
	file := filepath.Join(path, gostructName)
	generatedCode := genutil.OpenFile(file)
	defer genutil.SyncFile(generatedCode)

	// Write the package header to the supplier writer.
	fmt.Fprint(generatedCode, code.CommonHeader)
	fmt.Fprint(generatedCode, code.OneOffHeader)

	// Write the returned Go code out. First the Structs - which is the struct
	// definitions for the generated YANG entity, followed by the enumerations.
	for _, snippet := range code.Structs {
		fmt.Fprintln(generatedCode, snippet.String())
	}

	for _, snippet := range code.Enums {
		fmt.Fprintln(generatedCode, snippet)
	}

	// Write the generated enumeration map out.
	fmt.Fprintln(generatedCode, code.EnumMap)

	// Write the schema out if it was received.
	if len(code.JSONSchemaCode) > 0 {
		fmt.Fprintln(generatedCode, code.JSONSchemaCode)
	}

	if len(code.EnumTypeMap) > 0 {
		fmt.Fprintln(generatedCode, code.EnumTypeMap)
	}

	if err := writeManifest(path, &Manifest{
		Name:    "basic",
		Author:  "goSDN-Team",
		Version: "v1.0.0",
	}); err != nil {
		return err
	}

	return nil
}

// deprecated.
func writeCode(path string, code *gogen.GeneratedCode, t spb.Type) error {
	code.CommonHeader = strings.TrimSuffix(code.CommonHeader, ")\n")
	code.CommonHeader = code.CommonHeader + southboundImportAmendmend
	sbiStructCopy := southboundStruct
	if t == spb.Type_TYPE_CONTAINERISED {
		sbiStructCopy.Methods = sbiStructCopy.Methods + southboundStructCsbiAmendmend
	} else {
		sbiStructCopy.Methods = sbiStructCopy.Methods + southboundStructPluginAmendmend
	}
	code.Structs = append(code.Structs, sbiStructCopy)
	file := filepath.Join(path, gostructName)
	generatedCode := genutil.OpenFile(file)
	defer genutil.SyncFile(generatedCode)
	// Write the package header to the supplier writer.
	fmt.Fprint(generatedCode, code.CommonHeader)
	fmt.Fprint(generatedCode, code.OneOffHeader)

	// Write the returned Go code out. First the Structs - which is the struct
	// definitions for the generated YANG entity, followed by the enumerations.
	for _, snippet := range code.Structs {
		fmt.Fprintln(generatedCode, snippet)
	}

	for _, snippet := range code.Enums {
		fmt.Fprintln(generatedCode, snippet)
	}

	// Write the generated enumeration map out.
	fmt.Fprintln(generatedCode, code.EnumMap)

	// Write the schema out if it was received.
	if len(code.JSONSchemaCode) > 0 {
		fmt.Fprintln(generatedCode, code.JSONSchemaCode)
	}

	if len(code.EnumTypeMap) > 0 {
		fmt.Fprintln(generatedCode, code.EnumTypeMap)
	}

	if err := writeManifest(path, &Manifest{
		Name:    "basic",
		Author:  "goSDN-Team",
		Version: "v1.0.0",
	}); err != nil {
		return err
	}

	return nil
}

// Manifest represents a csbi manifest.
type Manifest struct {
	Name, Author, Version string
}

func writeManifest(path string, manifest *Manifest) error {
	m, err := yaml.Marshal(manifest)
	if err != nil {
		return err
	}
	if err := os.WriteFile(filepath.Join(path, manifestFileName), m, 0644); err != nil {
		return err
	}
	return nil
}
