package gnmidemo

import (
	"code.fbi.h-da.de/cocsn/gosdn/forks/goarista/gnmi"
	"code.fbi.h-da.de/cocsn/gosdn/nucleus"
	"code.fbi.h-da.de/cocsn/gosdn/nucleus/util"
	"code.fbi.h-da.de/danet/gnmi-demo/generated"
	"context"
	"errors"
	"fmt"
	"github.com/gogo/protobuf/proto"
	"github.com/google/uuid"
	gpb "github.com/openconfig/gnmi/proto/gnmi"
	"github.com/openconfig/goyang/pkg/yang"
	yutil "github.com/openconfig/ygot/util"
	"github.com/openconfig/ygot/ygot"
	"github.com/openconfig/ygot/ytypes"
	log "github.com/sirupsen/logrus"
	"os"
	"os/signal"
	"reflect"
	"syscall"
	"time"
)

var transport nucleus.Transport
var opts *nucleus.GnmiTransportOptions
var southbound sbi

func setup(params ...string) error {
	var err error
	southbound = sbi{}
	opts = &nucleus.GnmiTransportOptions{
		Addr:     params[0],
		Username: params[1],
		Password: params[2],
		SetNode:  southbound.SetNode(),
		RespChan: make(chan *gpb.SubscribeResponse),
		Encoding: 0,
	}
	transport, err = nucleus.NewGnmiTransport(opts)
	transport.(*nucleus.Gnmi).Unmarshal = southbound.Unmarshal()
	if err != nil {
		return err
	}
	return nil
}

func Set(params ...string) error {
	lenArgs := len(params) - 3
	if err := setup(params[lenArgs:]...); err != nil {
		return err
	}
	path := gnmi.SplitPath(params[0])
	req := []interface{}{
		&gnmi.Operation{
			Type:   "update",
			Origin: "",
			Target: "",
			Path:   path,
			Val:    params[1],
		},
	}
	resp, err := transport.Set(context.Background(), req...)
	if err != nil {
		return err
	}
	log.Info(resp)
	return nil
}

func Get(params ...string) error {
	if err := setup(params[1:]...); err != nil {
		return err
	}
	resp, err := transport.Get(context.Background(), params[0])
	if err != nil {
		return err
	}
	switch params[0] {
	case "/":
		if err := util.Write(resp.(proto.Message), "device"); err != nil {
			return err
		}
		log.Info("wrote root resource to file")
	default:
		device := &openconfig.Device{}
		blank := &gpb.GetResponse{}
		if err := util.Read("device", blank); err != nil {
			return err
		}
		if err := transport.ProcessResponse(blank, device, southbound.Schema()); err != nil {
			return err
		}
		if err := transport.ProcessResponse(resp, device, southbound.Schema()); err != nil {
			return err
		}
		log.Infof("response: %v", resp)
	}
	return nil
}

func Subscribe(params ...string) error {
	o := &gnmi.SubscribeOptions{
		UpdatesOnly:       false,
		Prefix:            "",
		Mode:              "stream",
		StreamMode:        "sample",
		SampleInterval:    uint64(10 * time.Second.Nanoseconds()),
		SuppressRedundant: false,
		HeartbeatInterval: uint64(time.Second.Nanoseconds()),
		Paths:             gnmi.SplitPaths(params[:1]),
		Origin:            "",
		Target:            opts.Addr,
	}
	done := make(chan os.Signal, 1)
	signal.Notify(done, syscall.SIGILL, syscall.SIGTERM)
	ctx := context.WithValue(context.Background(), "opts", o)
	go func() {
		if err := transport.Subscribe(ctx); err != nil {
			log.Fatal(err)
		}
	}()
	fmt.Println("awaiting signal")
	<-done
	fmt.Println("exiting")
	return nil
}

type sbi struct {
	schema *ytypes.Schema
}

func (s sbi) SbiIdentifier() string {
	panic("implement me")
}

func (s sbi) SetNode() func(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error {
	return func(schema *yang.Entry, root interface{}, path *gpb.Path, val interface{}, opts ...ytypes.SetNodeOpt) error {
		if err := ytypes.SetNode(schema, root.(*openconfig.Device), path, val, opts...); err != nil {
			return err
		}
		return nil
	}
}

func (s sbi) Schema() *ytypes.Schema {
	schema, err := openconfig.Schema()
	s.schema = schema
	if err != nil {
		log.Fatal(err)
	}
	return schema
}

// Unmarshal injects OpenConfig specific model
// representation to the transport.
// Needed for type assertion.
func (s sbi) Unmarshal() func([]byte, []string, interface{}, ...ytypes.UnmarshalOpt) error {
	return unmarshal
}

// unmarshal parses a root or 1st level gNMI response to a go struct
// Named return due to how recover works here
func unmarshal(bytes []byte, fields []string, goStruct interface{}, opt ...ytypes.UnmarshalOpt) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = r.(error)
		}
	}()
	switch l := len(fields); l {
	case 0:
		return openconfig.Unmarshal(bytes, goStruct.(*openconfig.Device), opt...)
	case 1:
	default:
		return errors.New("fehler")
	}
	var c ygot.GoStruct
	var field string

	// Load SBI definition
	d := openconfig.Device{}
	c, field, err = iter(&d, fields)
	if err != nil {
		return
	}
	if err = openconfig.Unmarshal(bytes, c, opt...); err != nil {
		return
	}
	reflect.ValueOf(goStruct.(*openconfig.Device)).Elem().FieldByName(field).Set(reflect.ValueOf(c))
	return nil
}

// iter walks down the provided paths and initializes the ygot.GoStruct. It only works for
// the root level. Watch out for named returns here
// TODO(mk): Fix deeper layers
func iter(a ygot.GoStruct, fields []string) (b ygot.GoStruct, f string, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = r.(error)
		}
	}()
	var c ygot.GoStruct
	var configStruct reflect.Value
	f = fields[0]
	s := reflect.ValueOf(a).Elem()
	h := s.FieldByName(f)
	configStruct = reflect.New(h.Type())

	// Pointer of field needs to be initialized.
	// Very convoluted russian doll trick
	// https://stackoverflow.com/a/57469950/4378176
	// https://golang.org/src/encoding/json/decode.go?s#L474
	// TODO(mk): Prettify
	p2 := configStruct.Elem()
	// If we have KeyHelperGoStruct we need make and modify map instead of plain struct
	if p2.Kind() == reflect.Map {
		p2.Set(reflect.MakeMap(p2.Type()))
		configStruct.Elem().Set(p2)
		if err := yutil.InsertIntoMapStructField(a, f, "", p2); err != nil {
			panic(err)
		}
	} else {
		configStruct.Elem().Set(reflect.New(p2.Type().Elem()))
		b = configStruct.Elem().Interface().(ygot.GoStruct)
	}
	if len(fields) > 1 {
		c, _, _ = iter(b, fields[1:])
	} else {
		return
	}
	reflect.ValueOf(b).Elem().FieldByName(f).Set(reflect.ValueOf(c))
	return
}

func (s sbi) Id() uuid.UUID {
	panic("implement me")
}